code wiki / wiki / nx_wiki_login_flow.nx

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}