nx_wiki_login_flow.nx source
↩ module page · 224 lines · 9147 B
1// nx_wiki_login_flow.nx -- minimal COMPLIANT admin login for the wiki.
2//
3// Fills the two gaps left in nx_wiki_admin_wiring (POST-verify + session
4// gate) by composing ONLY verified primitives. NO COOKIES (cardinal C1):
5// login returns an Ed25519-signed no-cookie token; the client holds it in
6// sessionStorage and sends X-Nishi-Session per request.
7//
8// COMPOSES (avoid-duplicate-primitives cardinal):
9// sha256 sha256_hash (passphrase hash)
10// hub/nx_no_cookie_session nx_ncs_mint_token / nx_ncs_validate_token
11// (verified end-to-end
12// 2026-05-29: mint+validate
13// +expiry+tamper all green)
14// hub/nx_no_cookie_session nx_ncs_derive_*_hash
15//
16// THREAT MODEL (V1, single admin): the admin passphrase hash is
17// sha256(passphrase). This is NOT memory-hard -- argon2id (nx_argon2id,
18// pending cross-validation) is the upgrade, tracked in the debt ledger as
19// B#login-pass-hash-not-memory-hard. For a single operator passphrase on a
20// sovereign LAN host this is the honest V1 floor, not a pretend-stub: it is
21// a real constant-time check, just not yet KSF-hardened.
22//
23// All loops use the FLAT increment idiom (single `i = i + 1`), never the
24// nested-if/early-exit-jump combo that the native compiler miscompiles
25// (T#native-loopvar-jump-plus-nested-incr).
26
27import "nx_syscalls.nx"
28import "sha256.nx"
29import "hub/nx_no_cookie_session.nx"
30import "nx_base64.nx"
31
32// ===== Sealed verdict surface (codes 2820-2829) =====
33const NX_WLOGIN_OK: i64 = 0
34const NX_WLOGIN_BAD_INPUT: i64 = 2820
35const NX_WLOGIN_UNAUTHORIZED: i64 = 2821
36const NX_WLOGIN_MINT_FAILED: i64 = 2822
37
38// Constant-time 32-byte compare. Returns 1 if equal, 0 otherwise.
39// Accumulates XOR diff over all bytes (no early exit -> no timing leak,
40// and no miscompiled control flow).
41func nx_wlogin_ct_eq32(a: *u8, b: *u8) -> i64 {
42 var diff: i64 = 0
43 var i: i64 = 0
44 while i < 32 {
45 diff = diff | ((a[i] as i64) ^ (b[i] as i64))
46 i = i + 1
47 }
48 if diff == 0 { return 1 }
49 return 0
50}
51
52// Verify a passphrase against the stored sha256 hash (constant-time).
53// Returns NX_WLOGIN_OK if it matches, NX_WLOGIN_UNAUTHORIZED otherwise.
54func nx_wlogin_verify_pass(
55 passphrase: *u8, passphrase_n: i64,
56 expected_hash_32: *u8
57) -> i64 {
58 if (passphrase as i64) == 0 { return 0 - NX_WLOGIN_BAD_INPUT }
59 if passphrase_n < 1 { return 0 - NX_WLOGIN_BAD_INPUT }
60 if (expected_hash_32 as i64) == 0 { return 0 - NX_WLOGIN_BAD_INPUT }
61 let got: *u8 = sys_mmap(32)
62 sha256_digest(passphrase, passphrase_n, got)
63 if nx_wlogin_ct_eq32(got, expected_hash_32) == 1 { return NX_WLOGIN_OK }
64 return 0 - NX_WLOGIN_UNAUTHORIZED
65}
66
67// Full login: verify passphrase, then mint a no-cookie session token.
68// On success writes a 152-byte token to token_out_152 and returns
69// NX_WLOGIN_OK. On a wrong passphrase returns NX_WLOGIN_UNAUTHORIZED and
70// writes NOTHING (no token leak).
71func nx_wlogin_login(
72 passphrase: *u8, passphrase_n: i64,
73 expected_hash_32: *u8,
74 server_priv_32: *u8,
75 realm_id: *u8, realm_id_n: i64,
76 user_handle: *u8, user_handle_n: i64,
77 now_unix_s: i64, ttl_s: i64,
78 token_out_152: *u8
79) -> i64 {
80 let vr: i64 = nx_wlogin_verify_pass(passphrase, passphrase_n, expected_hash_32)
81 if vr != NX_WLOGIN_OK { return 0 - NX_WLOGIN_UNAUTHORIZED }
82
83 let uid: *u8 = sys_mmap(32)
84 let rid: *u8 = sys_mmap(32)
85 if nx_ncs_derive_user_id_hash(realm_id, realm_id_n, user_handle, user_handle_n, uid) != NX_NCS_OK {
86 return 0 - NX_WLOGIN_MINT_FAILED
87 }
88 if nx_ncs_derive_realm_id_hash(realm_id, realm_id_n, rid) != NX_NCS_OK {
89 return 0 - NX_WLOGIN_MINT_FAILED
90 }
91 if nx_ncs_mint_token(server_priv_32, uid, rid, now_unix_s, ttl_s, token_out_152) != NX_NCS_OK {
92 return 0 - NX_WLOGIN_MINT_FAILED
93 }
94 return NX_WLOGIN_OK
95}
96
97// Session gate for protected routes: validate a no-cookie token against
98// the server pubkey + expected realm. Returns NX_WLOGIN_OK if the bearer
99// is authorized, NX_WLOGIN_UNAUTHORIZED otherwise. Thin wrapper over the
100// verified nx_ncs_validate_token so callers speak one verdict surface.
101func nx_wlogin_check_session(
102 server_pub_32: *u8,
103 token_152: *u8,
104 now_unix_s: i64,
105 realm_id: *u8, realm_id_n: i64
106) -> i64 {
107 if (token_152 as i64) == 0 { return 0 - NX_WLOGIN_UNAUTHORIZED }
108 let rid: *u8 = sys_mmap(32)
109 if nx_ncs_derive_realm_id_hash(realm_id, realm_id_n, rid) != NX_NCS_OK {
110 return 0 - NX_WLOGIN_BAD_INPUT
111 }
112 let v: i64 = nx_ncs_validate_token(server_pub_32, token_152, now_unix_s, rid, 0 as *u8)
113 if v != NX_NCS_OK { return 0 - NX_WLOGIN_UNAUTHORIZED }
114 return NX_WLOGIN_OK
115}
116
117// ===== V1 HTTP handler (closes T#login-not-wired-to-http) =====
118//
119// The no-cookie token is base64'd into / out of the X-Nishi-Session header
120// (NEVER a cookie, cardinal C1). The caller (daemon dispatcher) computes
121// is_login_post = (method==POST AND path=="/wiki/admin/login"), passes the
122// POST body (the passphrase) and the X-Nishi-Session header value; this
123// handler does verify->mint->encode on login and decode->validate on the gate.
124
125const NX_WLOGIN_RESP_CAP: i64 = 4096
126const NX_WLOGIN_TOKEN_B64_CAP: i64 = 256
127
128func _wlh_append(buf: *u8, off_p: *i64, cap: i64, src: *u8, n: i64) -> i64 {
129 if off_p[0] + n > cap { return 0 - NX_WLOGIN_BAD_INPUT }
130 var i: i64 = 0
131 while i < n {
132 buf[off_p[0] + i] = src[i]
133 i = i + 1
134 }
135 off_p[0] = off_p[0] + n
136 return NX_WLOGIN_OK
137}
138
139func _wlh_append_z(buf: *u8, off_p: *i64, cap: i64, z: *u8) -> i64 {
140 var n: i64 = 0
141 while z[n] != (0 as u8) { n = n + 1 }
142 return _wlh_append(buf, off_p, cap, z, n)
143}
144
145// Auth config bundled into one struct so the handler call stays under the
146// compiler's 16-arg IR cap. Caller (daemon boot) inits it once per realm.
147struct NxWloginAuth {
148 server_priv: *u8
149 server_pub: *u8
150 expected_hash: *u8
151 realm_id: *u8
152 realm_id_n: i64
153 user_handle: *u8
154 user_handle_n: i64
155 valid: i64
156}
157
158func nx_wlogin_auth_init(a: *NxWloginAuth, priv: *u8, pub: *u8, hash: *u8,
159 realm: *u8, realm_n: i64, handle: *u8, handle_n: i64) -> i64 {
160 if (a as i64) == 0 { return 0 - NX_WLOGIN_BAD_INPUT }
161 a.server_priv = priv
162 a.server_pub = pub
163 a.expected_hash = hash
164 a.realm_id = realm
165 a.realm_id_n = realm_n
166 a.user_handle = handle
167 a.user_handle_n = handle_n
168 a.valid = 1
169 return NX_WLOGIN_OK
170}
171
172func nx_wlogin_admin_handle(
173 a: *NxWloginAuth,
174 is_login_post: i64,
175 body: *u8, body_n: i64,
176 sess_b64: *u8, sess_b64_n: i64,
177 now_unix_s: i64, ttl_s: i64,
178 resp_buf: *u8, resp_cap: i64, out_resp_n: *i64
179) -> i64 {
180 if (a as i64) == 0 { return 0 - NX_WLOGIN_BAD_INPUT }
181 if a.valid != 1 { return 0 - NX_WLOGIN_BAD_INPUT }
182 if (resp_buf as i64) == 0 { return 0 - NX_WLOGIN_BAD_INPUT }
183 if (out_resp_n as i64) == 0 { return 0 - NX_WLOGIN_BAD_INPUT }
184 if resp_cap < NX_WLOGIN_RESP_CAP { return 0 - NX_WLOGIN_BAD_INPUT }
185 out_resp_n[0] = 0
186 let off: *i64 = (sys_mmap(8)) as *i64
187 off[0] = 0
188
189 // POST /wiki/admin/login: verify passphrase -> mint -> X-Nishi-Session.
190 if is_login_post == 1 {
191 let token: *u8 = sys_mmap(152)
192 let lrc: i64 = nx_wlogin_login(body, body_n, a.expected_hash, a.server_priv,
193 a.realm_id, a.realm_id_n, a.user_handle, a.user_handle_n,
194 now_unix_s, ttl_s, token)
195 if lrc != NX_WLOGIN_OK {
196 _wlh_append_z(resp_buf, off, resp_cap, "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nunauthorized\n" as *u8)
197 out_resp_n[0] = off[0]
198 return NX_WLOGIN_OK
199 }
200 let b64: *u8 = sys_mmap(NX_WLOGIN_TOKEN_B64_CAP)
201 let b64n: i64 = b64_encode(token, 152, b64)
202 _wlh_append_z(resp_buf, off, resp_cap, "HTTP/1.1 200 OK\r\nX-Nishi-Session: " as *u8)
203 _wlh_append(resp_buf, off, resp_cap, b64, b64n)
204 _wlh_append_z(resp_buf, off, resp_cap, "\r\nContent-Type: text/plain\r\nContent-Length: 9\r\n\r\nlogged-in" as *u8)
205 out_resp_n[0] = off[0]
206 return NX_WLOGIN_OK
207 }
208
209 // Gated path: require a valid X-Nishi-Session token (base64-decoded).
210 if sess_b64_n > 0 {
211 let tok: *u8 = sys_mmap(152)
212 let declen: i64 = b64_decode(sess_b64, sess_b64_n, tok)
213 if declen == 152 {
214 if nx_wlogin_check_session(a.server_pub, tok, now_unix_s, a.realm_id, a.realm_id_n) == NX_WLOGIN_OK {
215 _wlh_append_z(resp_buf, off, resp_cap, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\nok\n" as *u8)
216 out_resp_n[0] = off[0]
217 return NX_WLOGIN_OK
218 }
219 }
220 }
221 _wlh_append_z(resp_buf, off, resp_cap, "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nunauthorized\n" as *u8)
222 out_resp_n[0] = off[0]
223 return NX_WLOGIN_OK
224}