toiljs 0.0.52 → 0.0.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +2 -13
- package/build/client/auth.js +22 -86
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/docs/auth-todo.md +149 -0
- package/docs/auth.md +234 -173
- package/examples/basic/client/routes/pq.tsx +12 -6
- package/examples/basic/server/core/AppHandler.ts +6 -3
- package/examples/basic/server/main.ts +0 -1
- package/examples/basic/server/routes/Auth.ts +22 -51
- package/examples/basic/server/routes/Session.ts +5 -2
- package/package.json +1 -1
- package/server/globals/auth.ts +193 -99
- package/src/client/auth.ts +47 -157
- package/src/client/index.ts +2 -2
- package/src/compiler/generate.ts +1 -1
- package/src/devserver/crypto.ts +1 -1
- package/src/devserver/kv.ts +9 -12
- package/test/pqauth-e2e.test.ts +49 -4
- package/examples/basic/server/routes/PqDemo.ts +0 -127
|
@@ -4,7 +4,7 @@ import { DataReader, DataWriter } from 'data';
|
|
|
4
4
|
import { encodeSessionUser } from './Session';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Toil PQ-Auth: post-quantum password login, end-to-end and runnable under `toiljs dev`.
|
|
8
8
|
*
|
|
9
9
|
* The password never leaves the browser. The client blinds it through the
|
|
10
10
|
* server-keyed OPRF (precomputation-resistant keyed salt), stretches the OPRF
|
|
@@ -17,9 +17,9 @@ import { encodeSessionUser } from './Session';
|
|
|
17
17
|
*
|
|
18
18
|
* STORAGE: backed by the DEV-ONLY `kv.*` host imports (see
|
|
19
19
|
* `src/devserver/kv.ts`) so the register -> login chain spans requests under
|
|
20
|
-
* `toiljs dev`.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* `toiljs dev`. REMOVE LATER: this is a stand-in; once toildb is implemented,
|
|
21
|
+
* the Accounts/Challenges stores move onto it and this goes away. `kv.*` is not
|
|
22
|
+
* on the production edge.
|
|
23
23
|
*
|
|
24
24
|
* Wire: every body/response is binary (`DataWriter`/`DataReader`), never JSON.
|
|
25
25
|
*/
|
|
@@ -36,30 +36,10 @@ const DEMO_PAR: u32 = 1;
|
|
|
36
36
|
const CHALLENGE_TTL_SECS: u64 = 120;
|
|
37
37
|
const SESSION_TTL_SECS: u64 = 3600;
|
|
38
38
|
|
|
39
|
-
// DEV server ML-KEM-768 secret (decapsulation) key, hex. Matches the PINNED
|
|
40
|
-
// public key in `src/client/auth.ts`. DEV ONLY: a real deployment loads this
|
|
41
|
-
// from the secure env store and rotates it; never commit a real one.
|
|
42
|
-
const SERVER_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
|
|
43
|
-
|
|
44
|
-
// --- small helpers ----------------------------------------------------------
|
|
45
|
-
|
|
46
39
|
function utf8(s: string): Uint8Array {
|
|
47
40
|
return Uint8Array.wrap(String.UTF8.encode(s));
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
function fromHex(s: string): Uint8Array {
|
|
51
|
-
const out = new Uint8Array(s.length >> 1);
|
|
52
|
-
for (let i = 0; i < out.length; i++) {
|
|
53
|
-
out[i] = <u8>(hexNibble(s.charCodeAt(i * 2)) * 16 + hexNibble(s.charCodeAt(i * 2 + 1)));
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
function hexNibble(c: i32): i32 {
|
|
58
|
-
if (c >= 48 && c <= 57) return c - 48; // 0-9
|
|
59
|
-
if (c >= 97 && c <= 102) return c - 87; // a-f
|
|
60
|
-
if (c >= 65 && c <= 70) return c - 55; // A-F
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
43
|
function toHex(b: Uint8Array): string {
|
|
64
44
|
let s = '';
|
|
65
45
|
for (let i = 0; i < b.length; i++) {
|
|
@@ -96,20 +76,6 @@ function deriveSalt(username: string): Uint8Array {
|
|
|
96
76
|
return crypto.sha256Text('toil-demo-salt-v1:' + username).slice(0, 16);
|
|
97
77
|
}
|
|
98
78
|
|
|
99
|
-
// --- startup config (idempotent; re-applied per fresh instance) -------------
|
|
100
|
-
|
|
101
|
-
let __configured = false;
|
|
102
|
-
function ensureConfigured(): void {
|
|
103
|
-
if (__configured) return;
|
|
104
|
-
// OPRF master seed: 32 bytes from a fixed label (a real deployment uses a
|
|
105
|
-
// secret from the env store). Per-user OPRF keys derive from this + username.
|
|
106
|
-
AuthService.setOprfSeed(crypto.sha256Text('toil-demo-oprf-seed-v1'));
|
|
107
|
-
// Server static ML-KEM secret key (matches the client's pinned public key).
|
|
108
|
-
AuthService.setServerKemSecretKey(fromHex(SERVER_KEM_SK_HEX));
|
|
109
|
-
__configured = true;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// --- KV-backed storage (DEV-ONLY host imports; see src/devserver/kv.ts) ------
|
|
113
79
|
|
|
114
80
|
// @ts-ignore: decorator
|
|
115
81
|
@external('env', 'kv.put')
|
|
@@ -147,7 +113,6 @@ function chalKey(cid: Uint8Array): Uint8Array {
|
|
|
147
113
|
return utf8('chal:' + toHex(cid));
|
|
148
114
|
}
|
|
149
115
|
|
|
150
|
-
// --- records ----------------------------------------------------------------
|
|
151
116
|
|
|
152
117
|
class Account {
|
|
153
118
|
username: string = '';
|
|
@@ -219,7 +184,6 @@ class Auth {
|
|
|
219
184
|
* No taken-oracle: always succeeds; register/finish rejects a duplicate. */
|
|
220
185
|
@post('/register/start')
|
|
221
186
|
public registerStart(ctx: RouteContext): Response {
|
|
222
|
-
ensureConfigured();
|
|
223
187
|
const r = new DataReader(ctx.request.body);
|
|
224
188
|
const username = r.readString();
|
|
225
189
|
const blinded = r.readBytes();
|
|
@@ -238,17 +202,21 @@ class Auth {
|
|
|
238
202
|
}
|
|
239
203
|
|
|
240
204
|
/** POST /auth/register/finish body: str(username) bytes(pk) bytes(regProof)
|
|
241
|
-
* resp: u8(status)
|
|
205
|
+
* resp: u8(status) -- 0 = ok, 1 = username already registered. Verifies
|
|
206
|
+
* proof-of-possession before storing the key. */
|
|
242
207
|
@post('/register/finish')
|
|
243
208
|
public registerFinish(ctx: RouteContext): Response {
|
|
244
|
-
ensureConfigured();
|
|
245
209
|
const r = new DataReader(ctx.request.body);
|
|
246
210
|
const username = r.readString();
|
|
247
211
|
const pk = r.readBytes();
|
|
248
212
|
const proof = r.readBytes();
|
|
249
213
|
if (!r.ok) return fail();
|
|
250
214
|
if (pk.length != AuthService.PUBLIC_KEY_LEN) return fail();
|
|
251
|
-
|
|
215
|
+
// Already registered: a distinguishable status (not the generic 401) so the
|
|
216
|
+
// client can say "username taken, log in instead" rather than a blank error.
|
|
217
|
+
if (getAccount(username) != null) {
|
|
218
|
+
return Response.bytes(new DataWriter().writeU8(1).toBytes());
|
|
219
|
+
}
|
|
252
220
|
|
|
253
221
|
// Proof-of-possession: the client signed buildRegisterMessage with the
|
|
254
222
|
// matching secret key, so we confirm it actually holds it.
|
|
@@ -274,7 +242,6 @@ class Auth {
|
|
|
274
242
|
* and a fresh challenge -- a known and an unknown user are indistinguishable. */
|
|
275
243
|
@post('/login/start')
|
|
276
244
|
public loginStart(ctx: RouteContext): Response {
|
|
277
|
-
ensureConfigured();
|
|
278
245
|
const r = new DataReader(ctx.request.body);
|
|
279
246
|
const username = r.readString();
|
|
280
247
|
const blinded = r.readBytes();
|
|
@@ -318,7 +285,6 @@ class Auth {
|
|
|
318
285
|
* resp: u8(status) [+ bytes(sessionToken) bytes(serverConfirm)] + Set-Cookie */
|
|
319
286
|
@post('/login/finish')
|
|
320
287
|
public loginFinish(ctx: RouteContext): Response {
|
|
321
|
-
ensureConfigured();
|
|
322
288
|
const r = new DataReader(ctx.request.body);
|
|
323
289
|
const cid = r.readBytes();
|
|
324
290
|
const ct = r.readBytes();
|
|
@@ -330,19 +296,24 @@ class Auth {
|
|
|
330
296
|
if (ch == null) return fail();
|
|
331
297
|
if (nowSecs() >= ch.exp) return fail();
|
|
332
298
|
|
|
333
|
-
// 2. Rebuild the
|
|
334
|
-
// load the account key, verify
|
|
299
|
+
// 2. Rebuild the message from OUR stored values + the client's ct (and
|
|
300
|
+
// the bound params + server key id), load the account key, verify.
|
|
335
301
|
const acct = getAccount(ch.username);
|
|
336
302
|
if (acct == null) return fail();
|
|
337
|
-
const message = AuthService.
|
|
303
|
+
const message = AuthService.buildLoginMessage(
|
|
304
|
+
ch.username, AUD, cid, ch.nonce, ch.iat, ch.exp,
|
|
305
|
+
ct, DEMO_MEM_KIB, DEMO_ITERS, DEMO_PAR, AuthService.serverKemKeyId(),
|
|
306
|
+
);
|
|
338
307
|
if (!AuthService.verifyLogin(acct.publicKey, message, sig)) return fail();
|
|
339
308
|
|
|
340
|
-
// 3. Decapsulate
|
|
341
|
-
// build the
|
|
309
|
+
// 3. Decapsulate (proves WE hold the KEM key), derive the session key K
|
|
310
|
+
// bound to the transcript, and build the confirmation tag the client
|
|
311
|
+
// verifies for mutual auth.
|
|
342
312
|
const sharedSecret = AuthService.mlkemDecapsulate(ct);
|
|
343
313
|
if (sharedSecret.length != AuthService.SHARED_SECRET_LEN) return fail();
|
|
344
314
|
const transcriptHash = AuthService.sha256(message);
|
|
345
|
-
const
|
|
315
|
+
const sessionKey = AuthService.deriveSessionKey(sharedSecret, transcriptHash);
|
|
316
|
+
const confirm = AuthService.serverConfirmTag(sessionKey, transcriptHash);
|
|
346
317
|
|
|
347
318
|
// 4. Success: mint the session and return {0, sessionToken, confirm}.
|
|
348
319
|
const userData = encodeSessionUser(ch.username);
|
|
@@ -16,8 +16,11 @@ import { DataReader, DataWriter } from 'data';
|
|
|
16
16
|
* this `/session/dev-login` mints one for a caller-named demo user so the flow is
|
|
17
17
|
* runnable without the external account store the login example stubs out.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* No secret wiring is needed: `AuthService` reads `AUTH_SESSION_SECRET` from the
|
|
20
|
+
* env store automatically (with a clearly-insecure DEV fallback), so the cookie
|
|
21
|
+
* minted on login and re-verified here by the `@auth` gate always agree, even
|
|
22
|
+
* though each request runs in its own fresh wasm instance. A real deployment just
|
|
23
|
+
* sets `AUTH_SESSION_SECRET` in `.env.secrets`.
|
|
21
24
|
*/
|
|
22
25
|
|
|
23
26
|
// @user: the authenticated-user shape. Exactly one per program.
|
package/package.json
CHANGED
package/server/globals/auth.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// and the toiljs dev-server mock).
|
|
11
11
|
|
|
12
12
|
import { DataWriter, DataReader } from 'data';
|
|
13
|
+
import { HmacImportParams, HmacParams, ALG_SHA_256, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
Server,
|
|
@@ -69,34 +70,113 @@ declare function __toilVoprfEvaluate(
|
|
|
69
70
|
outPtr: usize,
|
|
70
71
|
): i32;
|
|
71
72
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
// Secret configuration is AUTOMATIC. Every secret below resolves on first use
|
|
74
|
+
// from, in order: (1) an explicit `set*()` override, (2) the tenant's env store
|
|
75
|
+
// (`Environment.getSecure`, backed by `.env.secrets` / the dashboard) under the
|
|
76
|
+
// key named below, then (3) a well-known, clearly-insecure DEV fallback so local
|
|
77
|
+
// dev and the examples run with zero config. The resolved value is cached in the
|
|
78
|
+
// module global. Because every request runs in a FRESH wasm instance that reads
|
|
79
|
+
// the SAME env value the SAME way, there is no per-route / per-instance secret to
|
|
80
|
+
// keep in sync by hand: a cookie minted by one route can never fail to verify in
|
|
81
|
+
// another for want of a `setSecret` call. A real deployment sets the env values
|
|
82
|
+
// (and pins its own server KEM public key in the client); the DEV fallbacks are
|
|
83
|
+
// never a place to put a real secret.
|
|
84
|
+
|
|
85
|
+
/** Env-store keys the framework reads automatically (tenant-set secrets). */
|
|
86
|
+
const ENV_SESSION_SECRET: string = 'AUTH_SESSION_SECRET';
|
|
87
|
+
const ENV_OPRF_SEED: string = 'AUTH_OPRF_SEED';
|
|
88
|
+
const ENV_KEM_SK: string = 'AUTH_KEM_SK';
|
|
89
|
+
|
|
90
|
+
/** Well-known DEV fallbacks (insecure; overridden by env or `set*()`). */
|
|
91
|
+
const DEV_SESSION_SECRET: string = 'toil-dev-insecure-session-secret-CHANGE-ME';
|
|
92
|
+
const DEV_OPRF_SEED_SRC: string = 'toil-dev-oprf-seed-v1';
|
|
93
|
+
// A well-known DEV ML-KEM-768 decapsulation key (hex). Its public half (the `ek`
|
|
94
|
+
// at bytes [1152, 2336) of the dk) is PINNED in the client (`src/client/auth.ts`,
|
|
95
|
+
// `SERVER_KEM_PUBLIC_KEY`), so the PQ-auth example runs with zero config. A real
|
|
96
|
+
// deployment sets `AUTH_KEM_SK` and pins its own public key; never treat this as
|
|
97
|
+
// secret. Tree-shaken away unless a server actually calls the ML-KEM path.
|
|
98
|
+
const DEV_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
|
|
99
|
+
|
|
100
|
+
// Resolved-and-cached per instance; `null` = not yet resolved and not overridden.
|
|
101
|
+
let __sessionSecret: Uint8Array | null = null;
|
|
102
|
+
let __oprfSeed: Uint8Array | null = null;
|
|
103
|
+
let __serverKemSk: Uint8Array | null = null;
|
|
104
|
+
let __serverKemPk: Uint8Array | null = null;
|
|
105
|
+
|
|
106
|
+
function __hexNibble(c: i32): i32 {
|
|
107
|
+
if (c >= 48 && c <= 57) return c - 48; // 0-9
|
|
108
|
+
if (c >= 97 && c <= 102) return c - 87; // a-f
|
|
109
|
+
if (c >= 65 && c <= 70) return c - 55; // A-F
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
function __fromHex(s: string): Uint8Array {
|
|
113
|
+
const out = new Uint8Array(s.length >> 1);
|
|
114
|
+
for (let i = 0; i < out.length; i++) {
|
|
115
|
+
out[i] = <u8>((__hexNibble(s.charCodeAt(i * 2)) << 4) | __hexNibble(s.charCodeAt(i * 2 + 1)));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The session HMAC secret (UTF-8 of the env value or the DEV fallback). */
|
|
121
|
+
function __resolveSessionSecret(): Uint8Array {
|
|
122
|
+
let s = __sessionSecret;
|
|
123
|
+
if (s != null) return s;
|
|
124
|
+
const v = Environment.getSecure(ENV_SESSION_SECRET);
|
|
125
|
+
s = Uint8Array.wrap(String.UTF8.encode(v != null ? v : DEV_SESSION_SECRET));
|
|
126
|
+
__sessionSecret = s;
|
|
127
|
+
return s;
|
|
128
|
+
}
|
|
129
|
+
/** The OPRF master seed, hashed to 32 bytes (RFC 9497 Ns) so any env value works. */
|
|
130
|
+
function __resolveOprfSeed(): Uint8Array {
|
|
131
|
+
let s = __oprfSeed;
|
|
132
|
+
if (s != null) return s;
|
|
133
|
+
const v = Environment.getSecure(ENV_OPRF_SEED);
|
|
134
|
+
s = crypto.sha256Text(v != null ? v : DEV_OPRF_SEED_SRC);
|
|
135
|
+
__oprfSeed = s;
|
|
136
|
+
return s;
|
|
137
|
+
}
|
|
138
|
+
/** The server static ML-KEM-768 secret (decapsulation) key (2400 bytes). */
|
|
139
|
+
function __resolveServerKemSk(): Uint8Array {
|
|
140
|
+
let s = __serverKemSk;
|
|
141
|
+
if (s != null) return s;
|
|
142
|
+
const v = Environment.getSecure(ENV_KEM_SK);
|
|
143
|
+
s = __fromHex(v != null ? v : DEV_KEM_SK_HEX);
|
|
144
|
+
__serverKemSk = s;
|
|
145
|
+
return s;
|
|
146
|
+
}
|
|
147
|
+
/** The server static ML-KEM-768 PUBLIC key (the `ek` embedded in the dk at bytes
|
|
148
|
+
* [1152, 2336), FIPS 203 layout), used for the key id bound into the login
|
|
149
|
+
* transcript. The client pins the same key. */
|
|
150
|
+
function __resolveServerKemPk(): Uint8Array {
|
|
151
|
+
let s = __serverKemPk;
|
|
152
|
+
if (s != null) return s;
|
|
153
|
+
s = __resolveServerKemSk().slice(1152, 2336);
|
|
154
|
+
__serverKemPk = s;
|
|
155
|
+
return s;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// HMAC-SHA256(key, msg) via the ambient Web Crypto (same path SecureCookies
|
|
159
|
+
// uses). The session-key derivation and the mutual-auth confirmation tag are
|
|
160
|
+
// both keyed PRFs over the transcript; the client mirrors this with hash-wasm.
|
|
161
|
+
function __hmacSha256(key: Uint8Array, msg: Uint8Array): Uint8Array {
|
|
162
|
+
const k = crypto.subtle.importKey(
|
|
163
|
+
'raw',
|
|
164
|
+
key,
|
|
165
|
+
new HmacImportParams(ALG_SHA_256),
|
|
166
|
+
false,
|
|
167
|
+
USAGE_SIGN | USAGE_VERIFY,
|
|
168
|
+
);
|
|
169
|
+
return crypto.subtle.sign(new HmacParams(), k, msg);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// `utf8(label) || transcriptHash` -- the HMAC message body for the derivations.
|
|
173
|
+
function __labelled(label: string, transcriptHash: Uint8Array): Uint8Array {
|
|
174
|
+
const lb = Uint8Array.wrap(String.UTF8.encode(label));
|
|
175
|
+
const buf = new Uint8Array(lb.length + transcriptHash.length);
|
|
176
|
+
buf.set(lb, 0);
|
|
177
|
+
buf.set(transcriptHash, lb.length);
|
|
178
|
+
return buf;
|
|
179
|
+
}
|
|
100
180
|
|
|
101
181
|
// Whether the current request arrived over HTTPS. A TLS edge / proxy signals it
|
|
102
182
|
// with `x-forwarded-proto: https`; absent (plain HTTP, including `toiljs dev`)
|
|
@@ -136,9 +216,10 @@ export namespace AuthService {
|
|
|
136
216
|
export const DEFAULT_SESSION_TTL_SECS: u64 = 86400; // 24h
|
|
137
217
|
|
|
138
218
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
219
|
+
* Override the session-signing secret programmatically. OPTIONAL: by default
|
|
220
|
+
* AuthService reads `AUTH_SESSION_SECRET` from the env store (with a DEV
|
|
221
|
+
* fallback), so most apps never call this. An override takes precedence over
|
|
222
|
+
* the env value for the current request; keep it out of any client bundle.
|
|
142
223
|
*/
|
|
143
224
|
export function setSecret(secret: Uint8Array): void {
|
|
144
225
|
__sessionSecret = secret;
|
|
@@ -154,7 +235,7 @@ export namespace AuthService {
|
|
|
154
235
|
const req = Server.currentRequest;
|
|
155
236
|
if (req == null) return null;
|
|
156
237
|
|
|
157
|
-
const sealed = SecureCookies.signed(
|
|
238
|
+
const sealed = SecureCookies.signed(__resolveSessionSecret()).open(
|
|
158
239
|
req.cookies(),
|
|
159
240
|
sessionCookieName(__reqIsSecure()),
|
|
160
241
|
);
|
|
@@ -217,7 +298,7 @@ export namespace AuthService {
|
|
|
217
298
|
.sameSite(SameSite.Lax)
|
|
218
299
|
.maxAge(<i64>ttlSecs);
|
|
219
300
|
cookie = secure ? cookie.asHostPrefixed() : cookie.path('/');
|
|
220
|
-
return SecureCookies.signed(
|
|
301
|
+
return SecureCookies.signed(__resolveSessionSecret()).seal(cookie);
|
|
221
302
|
}
|
|
222
303
|
|
|
223
304
|
/** A `Set-Cookie` that immediately clears the session (logout). */
|
|
@@ -273,17 +354,23 @@ export namespace AuthService {
|
|
|
273
354
|
|
|
274
355
|
/**
|
|
275
356
|
* Build the canonical login message `M` the client signs and the server
|
|
276
|
-
* verifies
|
|
277
|
-
* with its OWN stored values, never
|
|
278
|
-
*
|
|
357
|
+
* verifies. ONE fixed binary layout, no JSON and no version negotiation. The
|
|
358
|
+
* server MUST call this with its OWN stored values, never fields echoed by
|
|
359
|
+
* the client. Both ends produce byte-identical bytes via `DataWriter`:
|
|
279
360
|
*
|
|
280
|
-
* u8
|
|
281
|
-
* str
|
|
282
|
-
* str
|
|
283
|
-
* bytes cid
|
|
284
|
-
* bytes nonce
|
|
285
|
-
* u64
|
|
286
|
-
* u64
|
|
361
|
+
* u8 tag = 1 (format marker, not a compat switch)
|
|
362
|
+
* str sub (username)
|
|
363
|
+
* str aud (this service's audience; server-config constant)
|
|
364
|
+
* bytes cid (challenge id)
|
|
365
|
+
* bytes nonce (32 random bytes)
|
|
366
|
+
* u64 iat
|
|
367
|
+
* u64 exp
|
|
368
|
+
* bytes ct (ML-KEM ciphertext; binds the key encapsulation)
|
|
369
|
+
* u32 memKiB (Argon2id params, bound so a MITM cannot slip a
|
|
370
|
+
* u32 iterations downgrade past the signature)
|
|
371
|
+
* u32 parallelism
|
|
372
|
+
* bytes serverKemKeyId (SHA-256 of the server KEM public key; binds the
|
|
373
|
+
* server identity the client encapsulated to)
|
|
287
374
|
*/
|
|
288
375
|
export function buildLoginMessage(
|
|
289
376
|
sub: string,
|
|
@@ -292,6 +379,11 @@ export namespace AuthService {
|
|
|
292
379
|
nonce: Uint8Array,
|
|
293
380
|
iat: u64,
|
|
294
381
|
exp: u64,
|
|
382
|
+
ciphertext: Uint8Array,
|
|
383
|
+
memKiB: u32,
|
|
384
|
+
iterations: u32,
|
|
385
|
+
parallelism: u32,
|
|
386
|
+
serverKemKeyId: Uint8Array,
|
|
295
387
|
): Uint8Array {
|
|
296
388
|
const w = new DataWriter();
|
|
297
389
|
w.writeU8(1);
|
|
@@ -301,6 +393,11 @@ export namespace AuthService {
|
|
|
301
393
|
w.writeBytes(nonce);
|
|
302
394
|
w.writeU64(iat);
|
|
303
395
|
w.writeU64(exp);
|
|
396
|
+
w.writeBytes(ciphertext);
|
|
397
|
+
w.writeU32(memKiB);
|
|
398
|
+
w.writeU32(iterations);
|
|
399
|
+
w.writeU32(parallelism);
|
|
400
|
+
w.writeBytes(serverKemKeyId);
|
|
304
401
|
return w.toBytes();
|
|
305
402
|
}
|
|
306
403
|
|
|
@@ -328,11 +425,6 @@ export namespace AuthService {
|
|
|
328
425
|
return result == 1;
|
|
329
426
|
}
|
|
330
427
|
|
|
331
|
-
// ===================================================================
|
|
332
|
-
// Augmented-PAKE layer: OPRF keyed salt + ML-KEM mutual auth.
|
|
333
|
-
// (See server/globals/auth.ts header and the example `Auth` controller.)
|
|
334
|
-
// ===================================================================
|
|
335
|
-
|
|
336
428
|
/** ML-KEM-768 (FIPS 203) sizes. */
|
|
337
429
|
export const KEM_CIPHERTEXT_LEN: i32 = 1088;
|
|
338
430
|
export const KEM_SECRET_KEY_LEN: i32 = 2400;
|
|
@@ -344,23 +436,35 @@ export namespace AuthService {
|
|
|
344
436
|
export const OPRF_SEED_LEN: i32 = 32;
|
|
345
437
|
|
|
346
438
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
439
|
+
* Override the OPRF master seed (32 bytes) programmatically. OPTIONAL: by
|
|
440
|
+
* default AuthService reads `AUTH_OPRF_SEED` from the env store (hashed to 32
|
|
441
|
+
* bytes, with a DEV fallback). Per-user OPRF keys derive from this + the
|
|
442
|
+
* username; keep it out of any client bundle.
|
|
350
443
|
*/
|
|
351
444
|
export function setOprfSeed(seed: Uint8Array): void {
|
|
352
445
|
__oprfSeed = seed;
|
|
353
446
|
}
|
|
354
447
|
|
|
355
448
|
/**
|
|
356
|
-
*
|
|
357
|
-
* bytes).
|
|
358
|
-
*
|
|
449
|
+
* Override the server static ML-KEM-768 secret (decapsulation) key (2400
|
|
450
|
+
* bytes) programmatically. OPTIONAL: by default AuthService reads `AUTH_KEM_SK`
|
|
451
|
+
* (hex) from the env store, with a DEV fallback whose public half is pinned in
|
|
452
|
+
* the client. Never put this in a client bundle.
|
|
359
453
|
*/
|
|
360
454
|
export function setServerKemSecretKey(secretKey: Uint8Array): void {
|
|
361
455
|
__serverKemSk = secretKey;
|
|
362
456
|
}
|
|
363
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Override the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
|
|
460
|
+
* compute {@link serverKemKeyId}. OPTIONAL: by default it is derived from the
|
|
461
|
+
* secret key (the `ek` embedded at bytes [1152, 2336) of the dk), so setting
|
|
462
|
+
* the secret key is enough. Must be the key the client pins.
|
|
463
|
+
*/
|
|
464
|
+
export function setServerKemPublicKey(publicKey: Uint8Array): void {
|
|
465
|
+
__serverKemPk = publicKey;
|
|
466
|
+
}
|
|
467
|
+
|
|
364
468
|
/**
|
|
365
469
|
* OPRF server step: blind-evaluate the client's `blinded` element under the
|
|
366
470
|
* per-user key derived from the master seed + `username`. Returns the 32-byte
|
|
@@ -371,9 +475,10 @@ export namespace AuthService {
|
|
|
371
475
|
if (blinded.length != OPRF_ELEMENT_LEN) return new Uint8Array(0);
|
|
372
476
|
const info = Uint8Array.wrap(String.UTF8.encode(username));
|
|
373
477
|
const out = new Uint8Array(OPRF_ELEMENT_LEN);
|
|
478
|
+
const seed = __resolveOprfSeed();
|
|
374
479
|
const rc = __toilVoprfEvaluate(
|
|
375
|
-
|
|
376
|
-
|
|
480
|
+
seed.dataStart,
|
|
481
|
+
seed.length,
|
|
377
482
|
info.dataStart,
|
|
378
483
|
info.length,
|
|
379
484
|
blinded.dataStart,
|
|
@@ -389,15 +494,16 @@ export namespace AuthService {
|
|
|
389
494
|
* Only the genuine server can produce this, so it underpins mutual auth.
|
|
390
495
|
*/
|
|
391
496
|
export function mlkemDecapsulate(ciphertext: Uint8Array): Uint8Array {
|
|
392
|
-
|
|
497
|
+
const sk = __resolveServerKemSk();
|
|
498
|
+
if (sk.length != KEM_SECRET_KEY_LEN || ciphertext.length != KEM_CIPHERTEXT_LEN) {
|
|
393
499
|
return new Uint8Array(0);
|
|
394
500
|
}
|
|
395
501
|
const out = new Uint8Array(SHARED_SECRET_LEN);
|
|
396
502
|
const rc = __toilMlkemDecapsulate(
|
|
397
503
|
ciphertext.dataStart,
|
|
398
504
|
ciphertext.length,
|
|
399
|
-
|
|
400
|
-
|
|
505
|
+
sk.dataStart,
|
|
506
|
+
sk.length,
|
|
401
507
|
out.dataStart,
|
|
402
508
|
);
|
|
403
509
|
return rc == 0 ? out : new Uint8Array(0);
|
|
@@ -408,53 +514,41 @@ export namespace AuthService {
|
|
|
408
514
|
return crypto.subtle.digest('SHA-256', data);
|
|
409
515
|
}
|
|
410
516
|
|
|
411
|
-
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
*/
|
|
417
|
-
export function buildLoginMessageV2(
|
|
418
|
-
sub: string,
|
|
419
|
-
aud: string,
|
|
420
|
-
cid: Uint8Array,
|
|
421
|
-
nonce: Uint8Array,
|
|
422
|
-
iat: u64,
|
|
423
|
-
exp: u64,
|
|
424
|
-
ciphertext: Uint8Array,
|
|
425
|
-
): Uint8Array {
|
|
426
|
-
const w = new DataWriter();
|
|
427
|
-
w.writeU8(2);
|
|
428
|
-
w.writeString(sub);
|
|
429
|
-
w.writeString(aud);
|
|
430
|
-
w.writeBytes(cid);
|
|
431
|
-
w.writeBytes(nonce);
|
|
432
|
-
w.writeU64(iat);
|
|
433
|
-
w.writeU64(exp);
|
|
434
|
-
w.writeBytes(ciphertext);
|
|
435
|
-
return w.toBytes();
|
|
517
|
+
/** `SHA-256(serverKemPublicKey)` -- the key identity bound into the login
|
|
518
|
+
* message, so the signature commits to which server key the client
|
|
519
|
+
* encapsulated to. The client computes the same hash over its pinned key. */
|
|
520
|
+
export function serverKemKeyId(): Uint8Array {
|
|
521
|
+
return sha256(__resolveServerKemPk());
|
|
436
522
|
}
|
|
437
523
|
|
|
438
|
-
/** Domain
|
|
524
|
+
/** Domain separators for the session-key derivation and confirmation tag. */
|
|
525
|
+
export const SESSION_KEY_LABEL: string = 'toil-session-key-v1';
|
|
439
526
|
export const SERVER_CONFIRM_LABEL: string = 'toil-server-confirm-v1';
|
|
440
527
|
|
|
441
528
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
529
|
+
* Derive the authenticated session key `K` from the ML-KEM shared secret,
|
|
530
|
+
* bound to the transcript: `K = HMAC-SHA256(sharedSecret, SESSION_KEY_LABEL ||
|
|
531
|
+
* transcriptHash)`. The shared secret is already a uniform 32-byte key, so it
|
|
532
|
+
* keys the HMAC directly (an HKDF-Expand step). Both ends derive the same `K`
|
|
533
|
+
* iff the KEM exchange and transcript match.
|
|
446
534
|
*
|
|
447
|
-
* NOTE:
|
|
448
|
-
*
|
|
449
|
-
*
|
|
535
|
+
* NOTE: `K` is the handle for future channel binding. Binding the *session
|
|
536
|
+
* cookie* to the transport (so a stolen cookie is useless on another channel)
|
|
537
|
+
* needs the TLS exporter, which the wasm guest cannot see -- that is an
|
|
538
|
+
* edge/transport follow-up, not doable purely here.
|
|
539
|
+
*/
|
|
540
|
+
export function deriveSessionKey(sharedSecret: Uint8Array, transcriptHash: Uint8Array): Uint8Array {
|
|
541
|
+
return __hmacSha256(sharedSecret, __labelled(SESSION_KEY_LABEL, transcriptHash));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* The server's mutual-auth confirmation tag: `HMAC-SHA256(K, SERVER_CONFIRM_LABEL
|
|
546
|
+
* || transcriptHash)`, where `K` is {@link deriveSessionKey}. Only a server
|
|
547
|
+
* that decapsulated correctly (i.e. holds the KEM secret key) derives the same
|
|
548
|
+
* `K`, so the client verifying this tag proves the server's identity.
|
|
450
549
|
*/
|
|
451
|
-
export function serverConfirmTag(
|
|
452
|
-
|
|
453
|
-
const buf = new Uint8Array(label.length + sharedSecret.length + transcriptHash.length);
|
|
454
|
-
buf.set(label, 0);
|
|
455
|
-
buf.set(sharedSecret, label.length);
|
|
456
|
-
buf.set(transcriptHash, label.length + sharedSecret.length);
|
|
457
|
-
return crypto.subtle.digest('SHA-256', buf);
|
|
550
|
+
export function serverConfirmTag(sessionKey: Uint8Array, transcriptHash: Uint8Array): Uint8Array {
|
|
551
|
+
return __hmacSha256(sessionKey, __labelled(SERVER_CONFIRM_LABEL, transcriptHash));
|
|
458
552
|
}
|
|
459
553
|
|
|
460
554
|
/** Registration proof-of-possession context (binds a signature to "register"
|