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}