nx_opaque_pake.nx source
↩ module page · 538 lines · 23336 B
1// nx_opaque_pake.nx -- V-MODAUTH-2c: RFC 9807 OPAQUE-3DH top level (registration + login).
2//
3// THE auth capability the charter calls the crown jewel: the wiki (and every
4// Nishi site) gets password login where the server stores only a RegistrationRecord
5// that is useless for offline cracking without the separate oprf_seed, and login
6// produces a mutually-authenticated session_key without the password ever being
7// comparable server-side.
8//
9// V1 DEPLOYMENT HONESTY: until the NishiLang->wasm browser client ships the wire
10// PAKE (client-side Blind/KE1/KE3), the wiki daemon runs BOTH roles in-process on
11// the passphrase it received over TLS. What OPAQUE buys ALREADY in that mode:
12// nothing offline-crackable at rest, memory-hard KSF per guess, enumeration-resistant
13// masked responses, and the EXACT RFC wire formats exercised end-to-end -- so the
14// future wasm client speaks to this server with ZERO server change. The wire-PAKE
15// client is the named follow-on rung (V-MODAUTH-8, JS-last-mile lane).
16//
17// Deterministic cores (*_det) take ALL randomness as arguments = RFC C.1.5/C.1.6
18// vectors drive them byte-exact; wrappers draw CSPRNG.
19//
20// Message sizes (sealed, = RFC wire format):
21// RegistrationRequest 33 RegistrationResponse 66 RegistrationRecord 129
22// KE1 98 KE2 259 KE3 32
23//
24// COMPOSES: hub/nx_opaque_core, hub/nx_opaque_envelope, hub/nx_opaque_3dh,
25// hub/nx_voprf (+finalize), nx_csprng
26// COMPOSED BY: hub/nx_modern_auth_flow (register/login bodies), future wasm client
27// SPEC: RFC 9807 §5 (registration), §6 (online AKE), §6.3 (credential retrieval)
28// license_tier: ORIGINAL
29
30import "nx_syscalls.nx"
31import "nx_u256.nx"
32import "nx_csprng.nx"
33import "sha256.nx"
34import "nx_hmac.nx"
35import "nx_hkdf.nx"
36import "nx_p256_point.nx"
37import "nx_p256_scalar_mul.nx"
38import "hub/nx_voprf.nx"
39import "hub/nx_voprf_finalize.nx"
40import "hub/nx_h2c_p256.nx"
41import "hub/nx_opaque_core.nx"
42import "hub/nx_opaque_envelope.nx"
43import "hub/nx_opaque_3dh.nx"
44
45// ===== Sealed message sizes / record offsets =================================================
46const NX_OPQ_REG_REQUEST_BYTES: i64 = 33
47const NX_OPQ_REG_RESPONSE_BYTES: i64 = 66
48const NX_OPQ_RECORD_BYTES: i64 = 129
49const NX_OPQ_REC_OFF_CPUB: i64 = 0
50const NX_OPQ_REC_OFF_MASKKEY: i64 = 33
51const NX_OPQ_REC_OFF_ENV: i64 = 65
52
53// ===== Config structs (16-arg limit forces bundling) =================================================
54
55// Client-side configuration: identities (0-ptr => default to pubkeys), AKE context
56// string, and the KSF parameters. ONE struct reused by registration + login.
57struct NxOpqClientCfg {
58 server_id: *u8
59 server_id_n: i64
60 client_id: *u8
61 client_id_n: i64
62 context: *u8
63 context_n: i64
64 ksf_mode: i64 // NX_OPQ_KSF_IDENTITY (KAT only) | NX_OPQ_KSF_ARGON2ID
65 ksf_m_kib: i64
66 ksf_t: i64
67}
68
69// Server-side configuration: long-term AKE keypair, the global oprf_seed,
70// server identity, and the AKE context string.
71struct NxOpqServerCfg {
72 server_priv_32: *u8
73 server_pub_33: *u8
74 oprf_seed_32: *u8
75 server_id: *u8
76 server_id_n: i64
77 context: *u8
78 context_n: i64
79}
80
81// ===== Deterministic Blind (RFC 9497 §3.3.1 with caller-supplied blind) =====================
82//
83// nx_voprf_blind draws its own CSPRNG blind; the RFC vectors fix blind_registration /
84// blind_login, so the KAT needs this injection point. Production uses nx_voprf_blind.
85
86func nx_opq_blind_det(input: *u8, input_n: i64, blind_32: *u8, out_blinded_33: *u8) -> i64 {
87 if (input as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
88 if input_n < 0 { return 0 - NX_OPQ_BAD_INPUT }
89 if (blind_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
90 if (out_blinded_33 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
91 let dst: *u8 = sys_mmap(NX_VOPRF_DST_H2C_LEN)
92 _voprf_load_dst_h2c(dst)
93 let input_pt: *P256Point = p256_point_alloc()
94 let rc_h2c: i64 = nx_h2c_hash_to_curve_p256(input, input_n, dst, NX_VOPRF_DST_H2C_LEN, input_pt)
95 if rc_h2c != NX_H2C_OK { return 0 - NX_OPQ_OPRF_FAILED }
96 if p256_point_is_infinity(input_pt) == 1 { return 0 - NX_OPQ_OPRF_FAILED }
97 let blind_limbs: *i64 = u256_alloc()
98 u256_load_be(blind_limbs, blind_32)
99 if u256_is_zero(blind_limbs) == 1 { return 0 - NX_OPQ_BAD_INPUT }
100 let blinded_pt: *P256Point = p256_point_alloc()
101 p256_scalar_mul(blinded_pt, blind_limbs, input_pt)
102 if nx_voprf_serialize_element(blinded_pt, out_blinded_33) != NX_VOPRF_OK {
103 return 0 - NX_OPQ_OPRF_FAILED
104 }
105 return NX_OPQ_OK
106}
107
108// ===== Per-credential OPRF key per RFC 9807 §5.2.2 =================================================
109//
110// seed = Expand(oprf_seed, credential_identifier || "OprfKey", Nok)
111// (oprf_key, _) = DeriveKeyPair(seed, "OPAQUE-DeriveKeyPair")
112
113func nx_opq_derive_oprf_key(
114 oprf_seed_32: *u8,
115 cred_id: *u8, cred_id_n: i64,
116 out_oprf_key_32: *u8
117) -> i64 {
118 if (oprf_seed_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
119 if (cred_id as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
120 if cred_id_n < 1 { return 0 - NX_OPQ_BAD_INPUT }
121 if cred_id_n > NX_OPQ_MAX_ID_LEN { return 0 - NX_OPQ_BAD_INPUT }
122 if (out_oprf_key_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
123 let info: *u8 = sys_mmap(cred_id_n + 8)
124 var i: i64 = 0
125 while i < cred_id_n { info[i] = cred_id[i]; i = i + 1 }
126 let ok7: *u8 = "OprfKey" as *u8
127 var j: i64 = 0
128 while j < 7 { info[cred_id_n + j] = ok7[j]; j = j + 1 }
129 let seed: *u8 = sys_mmap(NX_OPQ_NOK)
130 if hkdf_expand(oprf_seed_32, info, cred_id_n + 7, NX_OPQ_NOK, seed) != 0 {
131 return 0 - NX_OPQ_DERIVE_FAILED
132 }
133 let dkp: *u8 = "OPAQUE-DeriveKeyPair" as *u8
134 return nx_opq_derive_keypair(seed, dkp, 20, out_oprf_key_32, 0 as *u8)
135}
136
137// ===== Registration per RFC 9807 §5 =================================================
138
139// Client step 1 (deterministic): request = blinded password element.
140func nx_opaque_create_registration_request_det(
141 password: *u8, password_n: i64,
142 blind_32: *u8,
143 out_request_33: *u8
144) -> i64 {
145 if password_n < 1 { return 0 - NX_OPQ_BAD_INPUT }
146 if password_n > NX_OPQ_MAX_PW_LEN { return 0 - NX_OPQ_BAD_INPUT }
147 return nx_opq_blind_det(password, password_n, blind_32, out_request_33)
148}
149
150// Client step 1 (production): random blind, returned for client state.
151func nx_opaque_create_registration_request(
152 password: *u8, password_n: i64,
153 out_blind_32: *u8,
154 out_request_33: *u8
155) -> i64 {
156 if password_n < 1 { return 0 - NX_OPQ_BAD_INPUT }
157 if password_n > NX_OPQ_MAX_PW_LEN { return 0 - NX_OPQ_BAD_INPUT }
158 if nx_voprf_blind(password, password_n, out_blind_32, out_request_33) != NX_VOPRF_OK {
159 return 0 - NX_OPQ_OPRF_FAILED
160 }
161 return NX_OPQ_OK
162}
163
164// Server step (fully deterministic by construction): evaluate under the
165// per-credential OPRF key, attach server public key.
166func nx_opaque_create_registration_response(
167 request_33: *u8,
168 server_pub_33: *u8,
169 cred_id: *u8, cred_id_n: i64,
170 oprf_seed_32: *u8,
171 out_response_66: *u8
172) -> i64 {
173 if (request_33 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
174 if (server_pub_33 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
175 if (out_response_66 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
176 let oprf_key: *u8 = sys_mmap(NX_OPQ_NOK)
177 let rc_k: i64 = nx_opq_derive_oprf_key(oprf_seed_32, cred_id, cred_id_n, oprf_key)
178 if rc_k != NX_OPQ_OK { return rc_k }
179 if nx_voprf_blind_evaluate(oprf_key, request_33, out_response_66) != NX_VOPRF_OK {
180 return 0 - NX_OPQ_OPRF_FAILED
181 }
182 var i: i64 = 0
183 while i < NX_OPQ_NPK { out_response_66[NX_OPQ_NOE + i] = server_pub_33[i]; i = i + 1 }
184 return NX_OPQ_OK
185}
186
187// Shared client derivation: OPRF Finalize -> KSF stretch -> randomized_password.
188func _opq_rwd_from_response(
189 ccfg: *NxOpqClientCfg,
190 password: *u8, password_n: i64,
191 blind_32: *u8,
192 evaluated_33: *u8,
193 out_rwd_32: *u8
194) -> i64 {
195 let oprf_out: *u8 = sys_mmap(NX_OPQ_NH)
196 if nx_voprf_finalize(password, password_n, blind_32, evaluated_33, oprf_out) != NX_VOPRF_OK {
197 return 0 - NX_OPQ_OPRF_FAILED
198 }
199 let stretched: *u8 = sys_mmap(NX_OPQ_NH)
200 let rc_s: i64 = nx_opq_stretch(oprf_out, ccfg.ksf_mode, ccfg.ksf_m_kib, ccfg.ksf_t, stretched)
201 if rc_s != NX_OPQ_OK { return rc_s }
202 return nx_opq_randomized_password(oprf_out, stretched, out_rwd_32)
203}
204
205// Client step 2 (deterministic core): build the RegistrationRecord the server stores.
206func nx_opaque_finalize_registration_det(
207 ccfg: *NxOpqClientCfg,
208 password: *u8, password_n: i64,
209 blind_32: *u8,
210 response_66: *u8,
211 envelope_nonce_32: *u8,
212 out_record_129: *u8,
213 out_export_key_32: *u8
214) -> i64 {
215 if (ccfg as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
216 if (response_66 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
217 if (out_record_129 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
218 if (out_export_key_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
219
220 let rwd: *u8 = sys_mmap(NX_OPQ_NH)
221 let rc_r: i64 = _opq_rwd_from_response(ccfg, password, password_n, blind_32,
222 response_66, rwd)
223 if rc_r != NX_OPQ_OK { return rc_r }
224
225 let server_pub: *u8 = (response_66 as i64 + NX_OPQ_NOE) as *u8
226 return nx_opq_envelope_store_det(rwd, server_pub,
227 ccfg.server_id, ccfg.server_id_n,
228 ccfg.client_id, ccfg.client_id_n,
229 envelope_nonce_32,
230 (out_record_129 as i64 + NX_OPQ_REC_OFF_ENV) as *u8,
231 (out_record_129 as i64 + NX_OPQ_REC_OFF_CPUB) as *u8,
232 (out_record_129 as i64 + NX_OPQ_REC_OFF_MASKKEY) as *u8,
233 out_export_key_32)
234}
235
236// Client step 2 (production): random envelope nonce.
237func nx_opaque_finalize_registration(
238 ccfg: *NxOpqClientCfg,
239 password: *u8, password_n: i64,
240 blind_32: *u8,
241 response_66: *u8,
242 out_record_129: *u8,
243 out_export_key_32: *u8
244) -> i64 {
245 let nonce: *u8 = sys_mmap(NX_OPQ_NN)
246 if nx_csprng_fill(nonce, NX_OPQ_NN) != 0 { return 0 - NX_OPQ_CSPRNG_FAILED }
247 return nx_opaque_finalize_registration_det(ccfg, password, password_n, blind_32,
248 response_66, nonce,
249 out_record_129, out_export_key_32)
250}
251
252// ===== Login per RFC 9807 §6 =================================================
253
254// Client: GenerateKE1 (deterministic core).
255// Client state the caller must hold for KE3: password, blind_32, out_client_secret_32, out_ke1_98.
256func nx_opaque_generate_ke1_det(
257 password: *u8, password_n: i64,
258 blind_32: *u8,
259 client_nonce_32: *u8,
260 client_keyshare_seed_32: *u8,
261 out_ke1_98: *u8,
262 out_client_secret_32: *u8
263) -> i64 {
264 if (out_ke1_98 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
265 if (out_client_secret_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
266 let rc_b: i64 = nx_opq_blind_det(password, password_n, blind_32,
267 (out_ke1_98 as i64 + NX_OPQ_KE1_OFF_BLINDED) as *u8)
268 if rc_b != NX_OPQ_OK { return rc_b }
269 var i: i64 = 0
270 while i < NX_OPQ_NN {
271 out_ke1_98[NX_OPQ_KE1_OFF_CNONCE + i] = client_nonce_32[i]
272 i = i + 1
273 }
274 let ddh: *u8 = "OPAQUE-DeriveDiffieHellmanKeyPair" as *u8
275 return nx_opq_derive_keypair(client_keyshare_seed_32, ddh, 33,
276 out_client_secret_32,
277 (out_ke1_98 as i64 + NX_OPQ_KE1_OFF_CKEYSHARE) as *u8)
278}
279
280// Client: GenerateKE1 (production): random blind + nonce + keyshare seed.
281func nx_opaque_generate_ke1(
282 password: *u8, password_n: i64,
283 out_blind_32: *u8,
284 out_ke1_98: *u8,
285 out_client_secret_32: *u8
286) -> i64 {
287 if nx_voprf_random_scalar(out_blind_32) != NX_VOPRF_OK { return 0 - NX_OPQ_CSPRNG_FAILED }
288 let nonce: *u8 = sys_mmap(NX_OPQ_NN)
289 if nx_csprng_fill(nonce, NX_OPQ_NN) != 0 { return 0 - NX_OPQ_CSPRNG_FAILED }
290 let seed: *u8 = sys_mmap(NX_OPQ_NSEED)
291 if nx_csprng_fill(seed, NX_OPQ_NSEED) != 0 { return 0 - NX_OPQ_CSPRNG_FAILED }
292 return nx_opaque_generate_ke1_det(password, password_n, out_blind_32,
293 nonce, seed, out_ke1_98, out_client_secret_32)
294}
295
296// Server: GenerateKE2 (deterministic core).
297// Outputs KE2 plus the server's session state: expected_client_mac + session_key.
298func nx_opaque_generate_ke2_det(
299 scfg: *NxOpqServerCfg,
300 record_129: *u8,
301 cred_id: *u8, cred_id_n: i64,
302 client_id: *u8, client_id_n: i64,
303 ke1_98: *u8,
304 masking_nonce_32: *u8,
305 server_nonce_32: *u8,
306 server_keyshare_seed_32: *u8,
307 out_ke2_259: *u8,
308 out_expected_client_mac_32: *u8,
309 out_session_key_32: *u8
310) -> i64 {
311 if (scfg as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
312 if (record_129 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
313 if (ke1_98 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
314 if (out_ke2_259 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
315 if (out_expected_client_mac_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
316 if (out_session_key_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
317
318 // --- CredentialResponse: evaluated_message ---
319 let oprf_key: *u8 = sys_mmap(NX_OPQ_NOK)
320 let rc_k: i64 = nx_opq_derive_oprf_key(scfg.oprf_seed_32, cred_id, cred_id_n, oprf_key)
321 if rc_k != NX_OPQ_OK { return rc_k }
322 if nx_voprf_blind_evaluate(oprf_key,
323 (ke1_98 as i64 + NX_OPQ_KE1_OFF_BLINDED) as *u8,
324 (out_ke2_259 as i64 + NX_OPQ_KE2_OFF_EVAL) as *u8) != NX_VOPRF_OK {
325 return 0 - NX_OPQ_OPRF_FAILED
326 }
327
328 // --- masking_nonce + masked_response = pad XOR (server_pub || envelope) ---
329 var n: i64 = 0
330 while n < NX_OPQ_NN {
331 out_ke2_259[NX_OPQ_KE2_OFF_MASKNONCE + n] = masking_nonce_32[n]
332 n = n + 1
333 }
334 let pad_info: *u8 = sys_mmap(NX_OPQ_NN + 24)
335 var pi: i64 = 0
336 while pi < NX_OPQ_NN { pad_info[pi] = masking_nonce_32[pi]; pi = pi + 1 }
337 let crp: *u8 = "CredentialResponsePad" as *u8
338 var ci: i64 = 0
339 while ci < 21 { pad_info[NX_OPQ_NN + ci] = crp[ci]; ci = ci + 1 }
340 let pad: *u8 = sys_mmap(NX_OPQ_MASKED_BYTES)
341 if hkdf_expand((record_129 as i64 + NX_OPQ_REC_OFF_MASKKEY) as *u8,
342 pad_info, NX_OPQ_NN + 21, NX_OPQ_MASKED_BYTES, pad) != 0 {
343 return 0 - NX_OPQ_DERIVE_FAILED
344 }
345 var m: i64 = 0
346 while m < NX_OPQ_NPK {
347 out_ke2_259[NX_OPQ_KE2_OFF_MASKED + m] = ((pad[m] as i64) ^ (scfg.server_pub_33[m] as i64)) as u8
348 m = m + 1
349 }
350 var e: i64 = 0
351 while e < NX_OPQ_ENVELOPE_BYTES {
352 out_ke2_259[NX_OPQ_KE2_OFF_MASKED + NX_OPQ_NPK + e] =
353 ((pad[NX_OPQ_NPK + e] as i64) ^ (record_129[NX_OPQ_REC_OFF_ENV + e] as i64)) as u8
354 e = e + 1
355 }
356
357 // --- AuthResponse: server nonce + ephemeral keyshare ---
358 var sn: i64 = 0
359 while sn < NX_OPQ_NN {
360 out_ke2_259[NX_OPQ_KE2_OFF_SNONCE + sn] = server_nonce_32[sn]
361 sn = sn + 1
362 }
363 let eph_sk: *u8 = sys_mmap(NX_OPQ_NSK)
364 let ddh: *u8 = "OPAQUE-DeriveDiffieHellmanKeyPair" as *u8
365 let rc_e: i64 = nx_opq_derive_keypair(server_keyshare_seed_32, ddh, 33, eph_sk,
366 (out_ke2_259 as i64 + NX_OPQ_KE2_OFF_SKEYSHARE) as *u8)
367 if rc_e != NX_OPQ_OK { return rc_e }
368
369 // --- identities resolved (default = pubkeys) ---
370 var sid: *u8 = scfg.server_id
371 var sid_n: i64 = scfg.server_id_n
372 if (sid as i64) == 0 { sid = scfg.server_pub_33; sid_n = NX_OPQ_NPK }
373 var cid: *u8 = client_id
374 var cid_n: i64 = client_id_n
375 if (cid as i64) == 0 {
376 cid = (record_129 as i64 + NX_OPQ_REC_OFF_CPUB) as *u8
377 cid_n = NX_OPQ_NPK
378 }
379
380 // --- preamble over ke2[0..227] (server_mac still unset; not in preamble) ---
381 let preamble: *u8 = sys_mmap(NX_OPQ_PREAMBLE_CAP)
382 let pre_n: i64 = nx_opq_preamble(scfg.context, scfg.context_n,
383 cid, cid_n, ke1_98, sid, sid_n,
384 out_ke2_259, preamble, NX_OPQ_PREAMBLE_CAP)
385 if pre_n < 0 { return pre_n }
386
387 // --- 3DH ikm: eph/eph, static/eph, eph/static ---
388 let ikm: *u8 = sys_mmap(NX_OPQ_IKM_BYTES)
389 let cks: *u8 = (ke1_98 as i64 + NX_OPQ_KE1_OFF_CKEYSHARE) as *u8
390 let rc1: i64 = nx_opq_dh(eph_sk, cks, ikm)
391 if rc1 != NX_OPQ_OK { return rc1 }
392 let rc2: i64 = nx_opq_dh(scfg.server_priv_32, cks, (ikm as i64 + 33) as *u8)
393 if rc2 != NX_OPQ_OK { return rc2 }
394 let rc3: i64 = nx_opq_dh(eph_sk, (record_129 as i64 + NX_OPQ_REC_OFF_CPUB) as *u8,
395 (ikm as i64 + 66) as *u8)
396 if rc3 != NX_OPQ_OK { return rc3 }
397
398 // --- key schedule + transcript MACs ---
399 let km2: *u8 = sys_mmap(NX_OPQ_NH)
400 let km3: *u8 = sys_mmap(NX_OPQ_NH)
401 let ph: *u8 = sys_mmap(NX_OPQ_NH)
402 let rc_d: i64 = nx_opq_derive_keys(ikm, preamble, pre_n, km2, km3, out_session_key_32, ph)
403 if rc_d != NX_OPQ_OK { return rc_d }
404 hmac_sha256(km2, NX_OPQ_NH, ph, NX_OPQ_NH, (out_ke2_259 as i64 + NX_OPQ_KE2_OFF_SMAC) as *u8)
405 return nx_opq_client_mac(km3, preamble, pre_n,
406 (out_ke2_259 as i64 + NX_OPQ_KE2_OFF_SMAC) as *u8,
407 out_expected_client_mac_32)
408}
409
410// Server: GenerateKE2 (production): random masking/server nonces + keyshare seed.
411func nx_opaque_generate_ke2(
412 scfg: *NxOpqServerCfg,
413 record_129: *u8,
414 cred_id: *u8, cred_id_n: i64,
415 client_id: *u8, client_id_n: i64,
416 ke1_98: *u8,
417 out_ke2_259: *u8,
418 out_expected_client_mac_32: *u8,
419 out_session_key_32: *u8
420) -> i64 {
421 let mn: *u8 = sys_mmap(NX_OPQ_NN)
422 if nx_csprng_fill(mn, NX_OPQ_NN) != 0 { return 0 - NX_OPQ_CSPRNG_FAILED }
423 let sn: *u8 = sys_mmap(NX_OPQ_NN)
424 if nx_csprng_fill(sn, NX_OPQ_NN) != 0 { return 0 - NX_OPQ_CSPRNG_FAILED }
425 let ks: *u8 = sys_mmap(NX_OPQ_NSEED)
426 if nx_csprng_fill(ks, NX_OPQ_NSEED) != 0 { return 0 - NX_OPQ_CSPRNG_FAILED }
427 return nx_opaque_generate_ke2_det(scfg, record_129, cred_id, cred_id_n,
428 client_id, client_id_n, ke1_98,
429 mn, sn, ks, out_ke2_259,
430 out_expected_client_mac_32, out_session_key_32)
431}
432
433// Client: GenerateKE3 -- recover credentials from KE2, verify server_mac,
434// emit client_mac + session_key + export_key.
435func nx_opaque_generate_ke3(
436 ccfg: *NxOpqClientCfg,
437 password: *u8, password_n: i64,
438 blind_32: *u8,
439 client_secret_32: *u8,
440 ke1_98: *u8,
441 ke2_259: *u8,
442 out_ke3_32: *u8,
443 out_session_key_32: *u8,
444 out_export_key_32: *u8
445) -> i64 {
446 if (ccfg as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
447 if (ke1_98 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
448 if (ke2_259 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
449 if (out_ke3_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
450 if (out_session_key_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
451 if (out_export_key_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
452
453 // --- randomized password from the evaluated message ---
454 let rwd: *u8 = sys_mmap(NX_OPQ_NH)
455 let rc_r: i64 = _opq_rwd_from_response(ccfg, password, password_n, blind_32,
456 (ke2_259 as i64 + NX_OPQ_KE2_OFF_EVAL) as *u8, rwd)
457 if rc_r != NX_OPQ_OK { return rc_r }
458
459 // --- unmask: pad XOR masked_response -> server_pub || envelope ---
460 let masking_key: *u8 = sys_mmap(NX_OPQ_NH)
461 let rc_m: i64 = nx_opq_masking_key(rwd, masking_key)
462 if rc_m != NX_OPQ_OK { return rc_m }
463 let pad_info: *u8 = sys_mmap(NX_OPQ_NN + 24)
464 var pi: i64 = 0
465 while pi < NX_OPQ_NN { pad_info[pi] = ke2_259[NX_OPQ_KE2_OFF_MASKNONCE + pi]; pi = pi + 1 }
466 let crp: *u8 = "CredentialResponsePad" as *u8
467 var ci: i64 = 0
468 while ci < 21 { pad_info[NX_OPQ_NN + ci] = crp[ci]; ci = ci + 1 }
469 let pad: *u8 = sys_mmap(NX_OPQ_MASKED_BYTES)
470 if hkdf_expand(masking_key, pad_info, NX_OPQ_NN + 21, NX_OPQ_MASKED_BYTES, pad) != 0 {
471 return 0 - NX_OPQ_DERIVE_FAILED
472 }
473 let unmasked: *u8 = sys_mmap(NX_OPQ_MASKED_BYTES)
474 var u: i64 = 0
475 while u < NX_OPQ_MASKED_BYTES {
476 unmasked[u] = ((pad[u] as i64) ^ (ke2_259[NX_OPQ_KE2_OFF_MASKED + u] as i64)) as u8
477 u = u + 1
478 }
479 let server_pub: *u8 = unmasked
480 let envelope: *u8 = (unmasked as i64 + NX_OPQ_NPK) as *u8
481
482 // --- recover client credentials (wrong password fails HERE) ---
483 let client_sk: *u8 = sys_mmap(NX_OPQ_NSK)
484 let client_pub: *u8 = sys_mmap(NX_OPQ_NPK)
485 let rc_rec: i64 = nx_opq_envelope_recover(rwd, server_pub, envelope,
486 ccfg.server_id, ccfg.server_id_n,
487 ccfg.client_id, ccfg.client_id_n,
488 client_sk, client_pub, out_export_key_32)
489 if rc_rec != NX_OPQ_OK { return rc_rec }
490
491 // --- identities resolved ---
492 var sid: *u8 = ccfg.server_id
493 var sid_n: i64 = ccfg.server_id_n
494 if (sid as i64) == 0 { sid = server_pub; sid_n = NX_OPQ_NPK }
495 var cid: *u8 = ccfg.client_id
496 var cid_n: i64 = ccfg.client_id_n
497 if (cid as i64) == 0 { cid = client_pub; cid_n = NX_OPQ_NPK }
498
499 // --- preamble + 3DH ikm (client side) ---
500 let preamble: *u8 = sys_mmap(NX_OPQ_PREAMBLE_CAP)
501 let pre_n: i64 = nx_opq_preamble(ccfg.context, ccfg.context_n,
502 cid, cid_n, ke1_98, sid, sid_n,
503 ke2_259, preamble, NX_OPQ_PREAMBLE_CAP)
504 if pre_n < 0 { return pre_n }
505 let sks: *u8 = (ke2_259 as i64 + NX_OPQ_KE2_OFF_SKEYSHARE) as *u8
506 let ikm: *u8 = sys_mmap(NX_OPQ_IKM_BYTES)
507 let rc1: i64 = nx_opq_dh(client_secret_32, sks, ikm)
508 if rc1 != NX_OPQ_OK { return rc1 }
509 let rc2: i64 = nx_opq_dh(client_secret_32, server_pub, (ikm as i64 + 33) as *u8)
510 if rc2 != NX_OPQ_OK { return rc2 }
511 let rc3: i64 = nx_opq_dh(client_sk, sks, (ikm as i64 + 66) as *u8)
512 if rc3 != NX_OPQ_OK { return rc3 }
513
514 // --- key schedule; verify server_mac; emit client_mac ---
515 let km2: *u8 = sys_mmap(NX_OPQ_NH)
516 let km3: *u8 = sys_mmap(NX_OPQ_NH)
517 let ph: *u8 = sys_mmap(NX_OPQ_NH)
518 let rc_d: i64 = nx_opq_derive_keys(ikm, preamble, pre_n, km2, km3, out_session_key_32, ph)
519 if rc_d != NX_OPQ_OK { return rc_d }
520 let expected_smac: *u8 = sys_mmap(NX_OPQ_NM)
521 hmac_sha256(km2, NX_OPQ_NH, ph, NX_OPQ_NH, expected_smac)
522 if nx_opq_ct_eq((ke2_259 as i64 + NX_OPQ_KE2_OFF_SMAC) as *u8, expected_smac, NX_OPQ_NM) != 1 {
523 var z: i64 = 0
524 while z < NX_OPQ_NH { out_session_key_32[z] = 0 as u8; out_export_key_32[z] = 0 as u8; z = z + 1 }
525 return 0 - NX_OPQ_SERVER_AUTH_FAIL
526 }
527 return nx_opq_client_mac(km3, preamble, pre_n, expected_smac, out_ke3_32)
528}
529
530// Server: ServerFinish per RFC 9807 §6.2.4 -- constant-time verify of KE3.
531func nx_opaque_server_finish(ke3_32: *u8, expected_client_mac_32: *u8) -> i64 {
532 if (ke3_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
533 if (expected_client_mac_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT }
534 if nx_opq_ct_eq(ke3_32, expected_client_mac_32, NX_OPQ_NM) != 1 {
535 return 0 - NX_OPQ_CLIENT_AUTH_FAIL
536 }
537 return NX_OPQ_OK
538}