code wiki / hub / nx_opaque_pake.nx

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}