code wiki / hub / nx_opaque_3dh.nx

nx_opaque_3dh.nx source

↩ module page · 180 lines · 7869 B

1// nx_opaque_3dh.nx -- V-MODAUTH-2b: RFC 9807 §6.4 OPAQUE-3DH key schedule. 2// 3// 3DH = triple Diffie-Hellman: three P-256 shared points (eph-eph, eph-static, 4// static-eph) concatenated into ikm, bound to the full transcript (preamble) via 5// HKDF. Gives forward secrecy + KCI resistance; KE2/KE3 MACs give mutual explicit 6// authentication. The password only enters through the envelope-recovered client 7// static key, so a wrong password fails loudly at envelope auth, never here. 8// 9// Wire layout (flat buffers; offsets sealed, they ARE the RFC wire format): 10// KE1 (98) = blinded_message(33) || client_nonce(32) || client_public_keyshare(33) 11// KE2 (259) = evaluated_message(33) || masking_nonce(32) || masked_response(97) 12// || server_nonce(32) || server_public_keyshare(33) || server_mac(32) 13// KE3 (32) = client_mac 14// 15// COMPOSES: hub/nx_opaque_core (DH, Expand-Label, ct_eq), nx_hkdf, nx_hmac, sha256 16// COMPOSED BY: hub/nx_opaque_pake (GenerateKE1/KE2/KE3, ServerFinish) 17// SPEC: RFC 9807 §6.4.2 (Preamble, DeriveKeys), §6.1 (message structs) 18// license_tier: ORIGINAL 19 20import "nx_syscalls.nx" 21import "sha256.nx" 22import "nx_hmac.nx" 23import "nx_hkdf.nx" 24import "hub/nx_opaque_core.nx" 25 26// ===== Sealed wire offsets ================================================= 27const NX_OPQ_KE1_BYTES: i64 = 98 28const NX_OPQ_KE1_OFF_BLINDED: i64 = 0 29const NX_OPQ_KE1_OFF_CNONCE: i64 = 33 30const NX_OPQ_KE1_OFF_CKEYSHARE: i64 = 65 31 32const NX_OPQ_KE2_BYTES: i64 = 259 33const NX_OPQ_KE2_OFF_EVAL: i64 = 0 34const NX_OPQ_KE2_OFF_MASKNONCE: i64 = 33 35const NX_OPQ_KE2_OFF_MASKED: i64 = 65 36const NX_OPQ_KE2_OFF_SNONCE: i64 = 162 37const NX_OPQ_KE2_OFF_SKEYSHARE: i64 = 194 38const NX_OPQ_KE2_OFF_SMAC: i64 = 227 39const NX_OPQ_CRED_RESP_BYTES: i64 = 162 // evaluated || masking_nonce || masked_response 40const NX_OPQ_MASKED_BYTES: i64 = 97 // Npk + Nn + Nm 41 42const NX_OPQ_KE3_BYTES: i64 = 32 43 44const NX_OPQ_PREAMBLE_CAP: i64 = 1024 45const NX_OPQ_IKM_BYTES: i64 = 99 // 3 * 33 compressed DH outputs 46 47// ===== Preamble per RFC 9807 §6.4.2.1 ================================================= 48// 49// preamble = "OPAQUEv1-" || I2OSP(len(context),2) || context 50// || I2OSP(len(client_identity),2) || client_identity 51// || ke1 52// || I2OSP(len(server_identity),2) || server_identity 53// || credential_response (ke2[0..162]) 54// || server_nonce (ke2[162..194]) 55// || server_public_keyshare (ke2[194..227]) 56// Identities are the RESOLVED ones (caller defaults them to pubkeys when unset). 57// Returns preamble length, or negative verdict. 58 59func nx_opq_preamble( 60 context: *u8, context_n: i64, 61 client_id: *u8, client_id_n: i64, 62 ke1_98: *u8, 63 server_id: *u8, server_id_n: i64, 64 ke2_259: *u8, 65 out: *u8, out_cap: i64 66) -> i64 { 67 if (ke1_98 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 68 if (ke2_259 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 69 if (out as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 70 if context_n < 0 { return 0 - NX_OPQ_BAD_INPUT } 71 if context_n > NX_OPQ_MAX_CONTEXT_LEN { return 0 - NX_OPQ_BAD_INPUT } 72 if client_id_n > NX_OPQ_MAX_ID_LEN { return 0 - NX_OPQ_BAD_INPUT } 73 if server_id_n > NX_OPQ_MAX_ID_LEN { return 0 - NX_OPQ_BAD_INPUT } 74 75 // (parser is newline-terminated: keep the sum on ONE line) 76 let total: i64 = 9 + 2 + context_n + 2 + client_id_n + NX_OPQ_KE1_BYTES + 2 + server_id_n + NX_OPQ_CRED_RESP_BYTES + NX_OPQ_NN + NX_OPQ_NPK 77 if total > out_cap { return 0 - NX_OPQ_BUF_OVERFLOW } 78 79 var pos: i64 = 0 80 let v1: *u8 = "OPAQUEv1-" as *u8 81 var i: i64 = 0 82 while i < 9 { out[i] = v1[i]; i = i + 1 } 83 pos = 9 84 out[pos] = ((context_n >> 8) & 0xFF) as u8 85 out[pos + 1] = (context_n & 0xFF) as u8 86 pos = pos + 2 87 var c: i64 = 0 88 while c < context_n { out[pos + c] = context[c]; c = c + 1 } 89 pos = pos + context_n 90 out[pos] = ((client_id_n >> 8) & 0xFF) as u8 91 out[pos + 1] = (client_id_n & 0xFF) as u8 92 pos = pos + 2 93 var d: i64 = 0 94 while d < client_id_n { out[pos + d] = client_id[d]; d = d + 1 } 95 pos = pos + client_id_n 96 var k: i64 = 0 97 while k < NX_OPQ_KE1_BYTES { out[pos + k] = ke1_98[k]; k = k + 1 } 98 pos = pos + NX_OPQ_KE1_BYTES 99 out[pos] = ((server_id_n >> 8) & 0xFF) as u8 100 out[pos + 1] = (server_id_n & 0xFF) as u8 101 pos = pos + 2 102 var s: i64 = 0 103 while s < server_id_n { out[pos + s] = server_id[s]; s = s + 1 } 104 pos = pos + server_id_n 105 // credential_response || server_nonce || server_public_keyshare = ke2[0..227] 106 var m: i64 = 0 107 while m < NX_OPQ_KE2_OFF_SMAC { out[pos + m] = ke2_259[m]; m = m + 1 } 108 pos = pos + NX_OPQ_KE2_OFF_SMAC 109 return pos 110} 111 112// ===== DeriveKeys per RFC 9807 §6.4.2.2 ================================================= 113// 114// prk = Extract("", ikm) 115// handshake_secret = Derive-Secret(prk, "HandshakeSecret", Hash(preamble)) 116// session_key = Derive-Secret(prk, "SessionKey", Hash(preamble)) 117// Km2 = Derive-Secret(handshake_secret, "ServerMAC", "") 118// Km3 = Derive-Secret(handshake_secret, "ClientMAC", "") 119// Also outputs Hash(preamble) so callers compute the transcript MACs without rehashing. 120 121func nx_opq_derive_keys( 122 ikm_99: *u8, 123 preamble: *u8, preamble_n: i64, 124 out_km2_32: *u8, 125 out_km3_32: *u8, 126 out_session_key_32: *u8, 127 out_preamble_hash_32: *u8 128) -> i64 { 129 if (ikm_99 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 130 if (preamble as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 131 if preamble_n < 1 { return 0 - NX_OPQ_BAD_INPUT } 132 if (out_km2_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 133 if (out_km3_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 134 if (out_session_key_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 135 if (out_preamble_hash_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 136 137 let prk: *u8 = sys_mmap(NX_OPQ_NH) 138 hkdf_extract(0 as *u8, 0, ikm_99, NX_OPQ_IKM_BYTES, prk) 139 sha256_digest(preamble, preamble_n, out_preamble_hash_32) 140 141 let hs: *u8 = sys_mmap(NX_OPQ_NX) 142 let lbl_hs: *u8 = "HandshakeSecret" as *u8 143 let rc1: i64 = nx_opq_derive_secret(prk, lbl_hs, 15, out_preamble_hash_32, NX_OPQ_NH, hs) 144 if rc1 != NX_OPQ_OK { return rc1 } 145 let lbl_sk: *u8 = "SessionKey" as *u8 146 let rc2: i64 = nx_opq_derive_secret(prk, lbl_sk, 10, out_preamble_hash_32, NX_OPQ_NH, out_session_key_32) 147 if rc2 != NX_OPQ_OK { return rc2 } 148 let lbl_sm: *u8 = "ServerMAC" as *u8 149 // zero-length context: pointer must be non-null but is never dereferenced (n=0) 150 let empty: *u8 = sys_mmap(1) 151 let rc3: i64 = nx_opq_derive_secret(hs, lbl_sm, 9, empty, 0, out_km2_32) 152 if rc3 != NX_OPQ_OK { return rc3 } 153 let lbl_cm: *u8 = "ClientMAC" as *u8 154 let rc4: i64 = nx_opq_derive_secret(hs, lbl_cm, 9, empty, 0, out_km3_32) 155 if rc4 != NX_OPQ_OK { return rc4 } 156 return NX_OPQ_OK 157} 158 159// server_mac = MAC(Km2, Hash(preamble)); client_mac = MAC(Km3, Hash(preamble || server_mac)). 160// Helper computes the client mac given the preamble hash chain inputs. 161func nx_opq_client_mac( 162 km3_32: *u8, 163 preamble: *u8, preamble_n: i64, 164 server_mac_32: *u8, 165 out_mac_32: *u8 166) -> i64 { 167 if (km3_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 168 if (preamble as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 169 if (server_mac_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 170 if (out_mac_32 as i64) == 0 { return 0 - NX_OPQ_BAD_INPUT } 171 let buf: *u8 = sys_mmap(preamble_n + NX_OPQ_NM) 172 var i: i64 = 0 173 while i < preamble_n { buf[i] = preamble[i]; i = i + 1 } 174 var j: i64 = 0 175 while j < NX_OPQ_NM { buf[preamble_n + j] = server_mac_32[j]; j = j + 1 } 176 let h: *u8 = sys_mmap(NX_OPQ_NH) 177 sha256_digest(buf, preamble_n + NX_OPQ_NM, h) 178 hmac_sha256(km3_32, NX_OPQ_NH, h, NX_OPQ_NH, out_mac_32) 179 return NX_OPQ_OK 180}