toiljs 0.0.52 → 0.0.53
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 +10 -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 +24 -3
- package/examples/basic/server/main.ts +0 -1
- package/examples/basic/server/routes/Auth.ts +56 -25
- package/examples/basic/server/routes/Session.ts +5 -2
- package/package.json +1 -1
- package/server/globals/auth.ts +93 -55
- 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,17 +36,25 @@ 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
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
39
|
+
// DEV fallback for the server ML-KEM-768 secret (decapsulation) key, hex, used
|
|
40
|
+
// only when `AUTH_KEM_SK` is not set in `.env.secrets`. Matches the PINNED public
|
|
41
|
+
// key in `src/client/auth.ts`. A real deployment sets AUTH_KEM_SK (and pins its
|
|
42
|
+
// own public key in the client) and rotates it; never ship a real one here.
|
|
43
|
+
const DEMO_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
|
|
43
44
|
|
|
44
|
-
// --- small helpers ----------------------------------------------------------
|
|
45
45
|
|
|
46
46
|
function utf8(s: string): Uint8Array {
|
|
47
47
|
return Uint8Array.wrap(String.UTF8.encode(s));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** A secret from the env store (`Environment.getSecure`, backed by `.env.secrets`),
|
|
51
|
+
* falling back to a DEV default so the example runs with zero config. Set the
|
|
52
|
+
* real value in `.env.secrets` for any non-throwaway use. */
|
|
53
|
+
function envSecretOr(key: string, devDefault: string): string {
|
|
54
|
+
const v = Environment.getSecure(key);
|
|
55
|
+
return v != null ? v : devDefault;
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
function fromHex(s: string): Uint8Array {
|
|
51
59
|
const out = new Uint8Array(s.length >> 1);
|
|
52
60
|
for (let i = 0; i < out.length; i++) {
|
|
@@ -96,20 +104,34 @@ function deriveSalt(username: string): Uint8Array {
|
|
|
96
104
|
return crypto.sha256Text('toil-demo-salt-v1:' + username).slice(0, 16);
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
// --- startup config (idempotent; re-applied per fresh instance) -------------
|
|
100
107
|
|
|
101
108
|
let __configured = false;
|
|
109
|
+
/** Lazily configure the auth-route crypto material (OPRF seed + server ML-KEM
|
|
110
|
+
* key) on first use; only the register/login handlers below read these.
|
|
111
|
+
*
|
|
112
|
+
* The session HMAC secret is deliberately NOT set here. The session is verified
|
|
113
|
+
* by the `@auth` gate in `routes/Session` (a DIFFERENT route -> a different fresh
|
|
114
|
+
* wasm instance per request), so it must be configured for EVERY request, not
|
|
115
|
+
* just auth ones. That happens once, before routing, in `core/AppHandler`. */
|
|
102
116
|
function ensureConfigured(): void {
|
|
103
117
|
if (__configured) return;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
// Both secrets come from the env store (`.env.secrets`), with DEV fallbacks
|
|
119
|
+
// so the example runs with zero config. Set the real values in `.env.secrets`
|
|
120
|
+
// (see `.env.secrets.example`) for any non-throwaway use.
|
|
121
|
+
|
|
122
|
+
// OPRF master seed: hashed to a 32-byte (RFC 9497 Ns) seed so any env value
|
|
123
|
+
// works. Per-user OPRF keys derive from this + the username.
|
|
124
|
+
AuthService.setOprfSeed(crypto.sha256Text(envSecretOr('AUTH_OPRF_SEED', 'toil-demo-oprf-seed-v1')));
|
|
125
|
+
// Server static ML-KEM secret key (must match the client's pinned public key).
|
|
126
|
+
const sk = fromHex(envSecretOr('AUTH_KEM_SK', DEMO_KEM_SK_HEX));
|
|
127
|
+
AuthService.setServerKemSecretKey(sk);
|
|
128
|
+
// The ML-KEM-768 public key (ek) is embedded in the decapsulation key at
|
|
129
|
+
// bytes [1152, 2336) (FIPS 203 dk layout); use it for the key id the login
|
|
130
|
+
// message binds. Identical to the public key the client pins.
|
|
131
|
+
AuthService.setServerKemPublicKey(sk.slice(1152, 2336));
|
|
109
132
|
__configured = true;
|
|
110
133
|
}
|
|
111
134
|
|
|
112
|
-
// --- KV-backed storage (DEV-ONLY host imports; see src/devserver/kv.ts) ------
|
|
113
135
|
|
|
114
136
|
// @ts-ignore: decorator
|
|
115
137
|
@external('env', 'kv.put')
|
|
@@ -147,7 +169,6 @@ function chalKey(cid: Uint8Array): Uint8Array {
|
|
|
147
169
|
return utf8('chal:' + toHex(cid));
|
|
148
170
|
}
|
|
149
171
|
|
|
150
|
-
// --- records ----------------------------------------------------------------
|
|
151
172
|
|
|
152
173
|
class Account {
|
|
153
174
|
username: string = '';
|
|
@@ -238,7 +259,8 @@ class Auth {
|
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
/** POST /auth/register/finish body: str(username) bytes(pk) bytes(regProof)
|
|
241
|
-
* resp: u8(status)
|
|
262
|
+
* resp: u8(status) -- 0 = ok, 1 = username already registered. Verifies
|
|
263
|
+
* proof-of-possession before storing the key. */
|
|
242
264
|
@post('/register/finish')
|
|
243
265
|
public registerFinish(ctx: RouteContext): Response {
|
|
244
266
|
ensureConfigured();
|
|
@@ -248,7 +270,11 @@ class Auth {
|
|
|
248
270
|
const proof = r.readBytes();
|
|
249
271
|
if (!r.ok) return fail();
|
|
250
272
|
if (pk.length != AuthService.PUBLIC_KEY_LEN) return fail();
|
|
251
|
-
|
|
273
|
+
// Already registered: a distinguishable status (not the generic 401) so the
|
|
274
|
+
// client can say "username taken, log in instead" rather than a blank error.
|
|
275
|
+
if (getAccount(username) != null) {
|
|
276
|
+
return Response.bytes(new DataWriter().writeU8(1).toBytes());
|
|
277
|
+
}
|
|
252
278
|
|
|
253
279
|
// Proof-of-possession: the client signed buildRegisterMessage with the
|
|
254
280
|
// matching secret key, so we confirm it actually holds it.
|
|
@@ -330,19 +356,24 @@ class Auth {
|
|
|
330
356
|
if (ch == null) return fail();
|
|
331
357
|
if (nowSecs() >= ch.exp) return fail();
|
|
332
358
|
|
|
333
|
-
// 2. Rebuild the
|
|
334
|
-
// load the account key, verify
|
|
359
|
+
// 2. Rebuild the message from OUR stored values + the client's ct (and
|
|
360
|
+
// the bound params + server key id), load the account key, verify.
|
|
335
361
|
const acct = getAccount(ch.username);
|
|
336
362
|
if (acct == null) return fail();
|
|
337
|
-
const message = AuthService.
|
|
363
|
+
const message = AuthService.buildLoginMessage(
|
|
364
|
+
ch.username, AUD, cid, ch.nonce, ch.iat, ch.exp,
|
|
365
|
+
ct, DEMO_MEM_KIB, DEMO_ITERS, DEMO_PAR, AuthService.serverKemKeyId(),
|
|
366
|
+
);
|
|
338
367
|
if (!AuthService.verifyLogin(acct.publicKey, message, sig)) return fail();
|
|
339
368
|
|
|
340
|
-
// 3. Decapsulate
|
|
341
|
-
// build the
|
|
369
|
+
// 3. Decapsulate (proves WE hold the KEM key), derive the session key K
|
|
370
|
+
// bound to the transcript, and build the confirmation tag the client
|
|
371
|
+
// verifies for mutual auth.
|
|
342
372
|
const sharedSecret = AuthService.mlkemDecapsulate(ct);
|
|
343
373
|
if (sharedSecret.length != AuthService.SHARED_SECRET_LEN) return fail();
|
|
344
374
|
const transcriptHash = AuthService.sha256(message);
|
|
345
|
-
const
|
|
375
|
+
const sessionKey = AuthService.deriveSessionKey(sharedSecret, transcriptHash);
|
|
376
|
+
const confirm = AuthService.serverConfirmTag(sessionKey, transcriptHash);
|
|
346
377
|
|
|
347
378
|
// 4. Success: mint the session and return {0, sessionToken, confirm}.
|
|
348
379
|
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
|
-
* The
|
|
20
|
-
*
|
|
19
|
+
* The session secret is configured once per request, BEFORE routing, in
|
|
20
|
+
* `core/AppHandler` (every request runs in a fresh wasm instance, so the secret
|
|
21
|
+
* must be set on each one, and the mint side and this `@auth` verify side are
|
|
22
|
+
* different routes). It reads `AUTH_SESSION_SECRET` from the env store with a
|
|
23
|
+
* clearly-insecure DEV fallback; a real deployment sets it 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,
|
|
@@ -98,6 +99,34 @@ let __oprfSeed: Uint8Array = Uint8Array.wrap(
|
|
|
98
99
|
// calls fail closed if unset.
|
|
99
100
|
let __serverKemSk: Uint8Array = new Uint8Array(0);
|
|
100
101
|
|
|
102
|
+
// Server static ML-KEM-768 PUBLIC (encapsulation) key, used only to compute the
|
|
103
|
+
// key identity bound into the login transcript (`serverKemKeyId`). The client
|
|
104
|
+
// pins the same key. Configured at startup via `setServerKemPublicKey`.
|
|
105
|
+
let __serverKemPk: Uint8Array = new Uint8Array(0);
|
|
106
|
+
|
|
107
|
+
// HMAC-SHA256(key, msg) via the ambient Web Crypto (same path SecureCookies
|
|
108
|
+
// uses). The session-key derivation and the mutual-auth confirmation tag are
|
|
109
|
+
// both keyed PRFs over the transcript; the client mirrors this with hash-wasm.
|
|
110
|
+
function __hmacSha256(key: Uint8Array, msg: Uint8Array): Uint8Array {
|
|
111
|
+
const k = crypto.subtle.importKey(
|
|
112
|
+
'raw',
|
|
113
|
+
key,
|
|
114
|
+
new HmacImportParams(ALG_SHA_256),
|
|
115
|
+
false,
|
|
116
|
+
USAGE_SIGN | USAGE_VERIFY,
|
|
117
|
+
);
|
|
118
|
+
return crypto.subtle.sign(new HmacParams(), k, msg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `utf8(label) || transcriptHash` -- the HMAC message body for the derivations.
|
|
122
|
+
function __labelled(label: string, transcriptHash: Uint8Array): Uint8Array {
|
|
123
|
+
const lb = Uint8Array.wrap(String.UTF8.encode(label));
|
|
124
|
+
const buf = new Uint8Array(lb.length + transcriptHash.length);
|
|
125
|
+
buf.set(lb, 0);
|
|
126
|
+
buf.set(transcriptHash, lb.length);
|
|
127
|
+
return buf;
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
// Whether the current request arrived over HTTPS. A TLS edge / proxy signals it
|
|
102
131
|
// with `x-forwarded-proto: https`; absent (plain HTTP, including `toiljs dev`)
|
|
103
132
|
// the session uses plain cookies so they actually round-trip in the browser.
|
|
@@ -273,17 +302,23 @@ export namespace AuthService {
|
|
|
273
302
|
|
|
274
303
|
/**
|
|
275
304
|
* Build the canonical login message `M` the client signs and the server
|
|
276
|
-
* verifies
|
|
277
|
-
* with its OWN stored values, never
|
|
278
|
-
*
|
|
305
|
+
* verifies. ONE fixed binary layout, no JSON and no version negotiation. The
|
|
306
|
+
* server MUST call this with its OWN stored values, never fields echoed by
|
|
307
|
+
* the client. Both ends produce byte-identical bytes via `DataWriter`:
|
|
279
308
|
*
|
|
280
|
-
* u8
|
|
281
|
-
* str
|
|
282
|
-
* str
|
|
283
|
-
* bytes cid
|
|
284
|
-
* bytes nonce
|
|
285
|
-
* u64
|
|
286
|
-
* u64
|
|
309
|
+
* u8 tag = 1 (format marker, not a compat switch)
|
|
310
|
+
* str sub (username)
|
|
311
|
+
* str aud (this service's audience; server-config constant)
|
|
312
|
+
* bytes cid (challenge id)
|
|
313
|
+
* bytes nonce (32 random bytes)
|
|
314
|
+
* u64 iat
|
|
315
|
+
* u64 exp
|
|
316
|
+
* bytes ct (ML-KEM ciphertext; binds the key encapsulation)
|
|
317
|
+
* u32 memKiB (Argon2id params, bound so a MITM cannot slip a
|
|
318
|
+
* u32 iterations downgrade past the signature)
|
|
319
|
+
* u32 parallelism
|
|
320
|
+
* bytes serverKemKeyId (SHA-256 of the server KEM public key; binds the
|
|
321
|
+
* server identity the client encapsulated to)
|
|
287
322
|
*/
|
|
288
323
|
export function buildLoginMessage(
|
|
289
324
|
sub: string,
|
|
@@ -292,6 +327,11 @@ export namespace AuthService {
|
|
|
292
327
|
nonce: Uint8Array,
|
|
293
328
|
iat: u64,
|
|
294
329
|
exp: u64,
|
|
330
|
+
ciphertext: Uint8Array,
|
|
331
|
+
memKiB: u32,
|
|
332
|
+
iterations: u32,
|
|
333
|
+
parallelism: u32,
|
|
334
|
+
serverKemKeyId: Uint8Array,
|
|
295
335
|
): Uint8Array {
|
|
296
336
|
const w = new DataWriter();
|
|
297
337
|
w.writeU8(1);
|
|
@@ -301,6 +341,11 @@ export namespace AuthService {
|
|
|
301
341
|
w.writeBytes(nonce);
|
|
302
342
|
w.writeU64(iat);
|
|
303
343
|
w.writeU64(exp);
|
|
344
|
+
w.writeBytes(ciphertext);
|
|
345
|
+
w.writeU32(memKiB);
|
|
346
|
+
w.writeU32(iterations);
|
|
347
|
+
w.writeU32(parallelism);
|
|
348
|
+
w.writeBytes(serverKemKeyId);
|
|
304
349
|
return w.toBytes();
|
|
305
350
|
}
|
|
306
351
|
|
|
@@ -328,11 +373,6 @@ export namespace AuthService {
|
|
|
328
373
|
return result == 1;
|
|
329
374
|
}
|
|
330
375
|
|
|
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
376
|
/** ML-KEM-768 (FIPS 203) sizes. */
|
|
337
377
|
export const KEM_CIPHERTEXT_LEN: i32 = 1088;
|
|
338
378
|
export const KEM_SECRET_KEY_LEN: i32 = 2400;
|
|
@@ -361,6 +401,16 @@ export namespace AuthService {
|
|
|
361
401
|
__serverKemSk = secretKey;
|
|
362
402
|
}
|
|
363
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Configure the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
|
|
406
|
+
* compute {@link serverKemKeyId}. Must be the key the client pins. (It is the
|
|
407
|
+
* `ek` embedded in the decapsulation key, so a tenant can pass
|
|
408
|
+
* `secretKey.slice(1152, 2336)` rather than store it twice.)
|
|
409
|
+
*/
|
|
410
|
+
export function setServerKemPublicKey(publicKey: Uint8Array): void {
|
|
411
|
+
__serverKemPk = publicKey;
|
|
412
|
+
}
|
|
413
|
+
|
|
364
414
|
/**
|
|
365
415
|
* OPRF server step: blind-evaluate the client's `blinded` element under the
|
|
366
416
|
* per-user key derived from the master seed + `username`. Returns the 32-byte
|
|
@@ -408,53 +458,41 @@ export namespace AuthService {
|
|
|
408
458
|
return crypto.subtle.digest('SHA-256', data);
|
|
409
459
|
}
|
|
410
460
|
|
|
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();
|
|
461
|
+
/** `SHA-256(serverKemPublicKey)` -- the key identity bound into the login
|
|
462
|
+
* message, so the signature commits to which server key the client
|
|
463
|
+
* encapsulated to. The client computes the same hash over its pinned key. */
|
|
464
|
+
export function serverKemKeyId(): Uint8Array {
|
|
465
|
+
return sha256(__serverKemPk);
|
|
436
466
|
}
|
|
437
467
|
|
|
438
|
-
/** Domain
|
|
468
|
+
/** Domain separators for the session-key derivation and confirmation tag. */
|
|
469
|
+
export const SESSION_KEY_LABEL: string = 'toil-session-key-v1';
|
|
439
470
|
export const SERVER_CONFIRM_LABEL: string = 'toil-server-confirm-v1';
|
|
440
471
|
|
|
441
472
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
473
|
+
* Derive the authenticated session key `K` from the ML-KEM shared secret,
|
|
474
|
+
* bound to the transcript: `K = HMAC-SHA256(sharedSecret, SESSION_KEY_LABEL ||
|
|
475
|
+
* transcriptHash)`. The shared secret is already a uniform 32-byte key, so it
|
|
476
|
+
* keys the HMAC directly (an HKDF-Expand step). Both ends derive the same `K`
|
|
477
|
+
* iff the KEM exchange and transcript match.
|
|
446
478
|
*
|
|
447
|
-
* NOTE:
|
|
448
|
-
*
|
|
449
|
-
*
|
|
479
|
+
* NOTE: `K` is the handle for future channel binding. Binding the *session
|
|
480
|
+
* cookie* to the transport (so a stolen cookie is useless on another channel)
|
|
481
|
+
* needs the TLS exporter, which the wasm guest cannot see -- that is an
|
|
482
|
+
* edge/transport follow-up, not doable purely here.
|
|
483
|
+
*/
|
|
484
|
+
export function deriveSessionKey(sharedSecret: Uint8Array, transcriptHash: Uint8Array): Uint8Array {
|
|
485
|
+
return __hmacSha256(sharedSecret, __labelled(SESSION_KEY_LABEL, transcriptHash));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The server's mutual-auth confirmation tag: `HMAC-SHA256(K, SERVER_CONFIRM_LABEL
|
|
490
|
+
* || transcriptHash)`, where `K` is {@link deriveSessionKey}. Only a server
|
|
491
|
+
* that decapsulated correctly (i.e. holds the KEM secret key) derives the same
|
|
492
|
+
* `K`, so the client verifying this tag proves the server's identity.
|
|
450
493
|
*/
|
|
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);
|
|
494
|
+
export function serverConfirmTag(sessionKey: Uint8Array, transcriptHash: Uint8Array): Uint8Array {
|
|
495
|
+
return __hmacSha256(sessionKey, __labelled(SERVER_CONFIRM_LABEL, transcriptHash));
|
|
458
496
|
}
|
|
459
497
|
|
|
460
498
|
/** Registration proof-of-possession context (binds a signature to "register"
|