code wiki / (root) / nx_basic_auth.nx

nx_basic_auth.nx source

↩ module page · 201 lines · 6598 B

1// basic_auth.nx -- HTTP Basic Authentication (RFC 7617). 2// 3// Authorization header format: 4// Authorization: Basic BASE64(username:password) 5// 6// The oldest HTTP auth scheme. Still everywhere: admin-panel 7// login, IoT device configs, CI runner tokens, registry push, 8// htpasswd-protected directories, internal tools. Combined with 9// HTTPS it's adequate; on plain HTTP it's trivially sniffable. 10// 11// We ship encoder + decoder + Authorization-header matcher. 12// Composes base64.nx. 13// 14// Invariants: 15// BA1 Encoded output is \"Basic \" + base64(\"user:pass\"), one 16// ASCII line. 17// BA2 Decoder strips the leading \"Basic \" prefix case- 18// insensitively per RFC 7617 ยง2. 19// BA3 username + password round-trip byte-exact. 20// BA4 Compare uses constant-time memcmp via ct.nx for the 21// password half -- timing attacks on admin logins are 22// real. 23 24// nx_safety_envelope: 25// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 26// sil_target: SIL1 27// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 28// verdict: NOT_YET_EVALUATED 29 30import "nx_syscalls.nx" 31import "nx_base64.nx" 32import "nx_ct.nx" 33 34const BA_ERR_FORMAT: i64 = -1 35const BA_ERR_SHORT: i64 = -2 36 37// Encode username + password into an Authorization header VALUE 38// (the part after the header name, e.g. \"Basic dXNlcjpwYXNz\"). 39// Returns bytes written or BA_ERR_SHORT. 40func basic_auth_encode(username: *u8, u_len: i64, 41 password: *u8, p_len: i64, 42 out: *u8, cap: i64) -> i64 { 43 // First write \"Basic \". 44 let prefix_len: i64 = 6 45 if cap < prefix_len { return BA_ERR_SHORT } 46 out[0] = 0x42 // 'B' 47 out[1] = 0x61 // 'a' 48 out[2] = 0x73 // 's' 49 out[3] = 0x69 // 'i' 50 out[4] = 0x63 // 'c' 51 out[5] = 0x20 // ' ' 52 53 // Build \"user:pass\" in scratch. 54 let scratch_len: i64 = u_len + 1 + p_len 55 let scratch: *u8 = sys_mmap(scratch_len + 16) 56 var i: i64 = 0 57 while i < u_len { 58 scratch[i] = username[i] 59 i = i + 1 60 } 61 scratch[u_len] = 0x3A 62 i = 0 63 while i < p_len { 64 scratch[u_len + 1 + i] = password[i] 65 i = i + 1 66 } 67 68 // Base64 after prefix. 69 let b64_cap: i64 = cap - prefix_len 70 let b64_len: i64 = b64_encode(scratch, scratch_len, out + prefix_len) 71 if b64_len < 0 { return BA_ERR_SHORT } 72 if b64_len > b64_cap { return BA_ERR_SHORT } 73 return prefix_len + b64_len 74} 75 76// Decode an Authorization header value. Writes username to 77// u_out, password to p_out; lengths to u_len_out, p_len_out. 78// Returns 0 on success, negative on failure. 79func basic_auth_decode(hdr: *u8, n: i64, 80 u_out: *u8, u_cap: i64, u_len_out: *i64, 81 p_out: *u8, p_cap: i64, p_len_out: *i64) -> i64 { 82 if n < 7 { return BA_ERR_SHORT } // \"Basic X\" min 83 84 // Match \"Basic \" case-insensitively. 85 let b: i64 = hdr[0] 86 let a: i64 = hdr[1] 87 let s: i64 = hdr[2] 88 let i_: i64 = hdr[3] 89 let c: i64 = hdr[4] 90 let sp: i64 = hdr[5] 91 // Lowercase-compare first five. 92 if (b | 0x20) != 0x62 { return BA_ERR_FORMAT } 93 if (a | 0x20) != 0x61 { return BA_ERR_FORMAT } 94 if (s | 0x20) != 0x73 { return BA_ERR_FORMAT } 95 if (i_ | 0x20) != 0x69 { return BA_ERR_FORMAT } 96 if (c | 0x20) != 0x63 { return BA_ERR_FORMAT } 97 if sp != 0x20 { return BA_ERR_FORMAT } 98 99 // Base64-decode the remainder. 100 let b64_off: i64 = 6 101 let b64_len: i64 = n - 6 102 // Worst-case decoded size = ceil(b64_len * 3 / 4). 103 let scratch_cap: i64 = b64_len + 4 104 let scratch: *u8 = sys_mmap(scratch_cap + 16) 105 let decoded_len: i64 = b64_decode(hdr + b64_off, b64_len, scratch) 106 if decoded_len < 0 { return BA_ERR_FORMAT } 107 108 // Split at first ':'. 109 var colon: i64 = -1 110 var i: i64 = 0 111 while i < decoded_len { 112 if scratch[i] == 0x3A { 113 colon = i 114 break 115 } 116 i = i + 1 117 } 118 if colon < 0 { return BA_ERR_FORMAT } 119 120 let u_len: i64 = colon 121 let p_len: i64 = decoded_len - colon - 1 122 if u_len > u_cap { return BA_ERR_SHORT } 123 if p_len > p_cap { return BA_ERR_SHORT } 124 125 i = 0 126 while i < u_len { 127 u_out[i] = scratch[i] 128 i = i + 1 129 } 130 i = 0 131 while i < p_len { 132 p_out[i] = scratch[colon + 1 + i] 133 i = i + 1 134 } 135 *u_len_out = u_len 136 *p_len_out = p_len 137 return 0 138} 139 140// Constant-time check: does (user_got, pass_got) match (user_want, 141// pass_want)? Returns 1 on match, 0 otherwise. Critical for 142// admin login flows -- naive memcmp leaks timing. 143func basic_auth_verify(user_got: *u8, u_got_len: i64, 144 pass_got: *u8, p_got_len: i64, 145 user_want: *u8, u_want_len: i64, 146 pass_want: *u8, p_want_len: i64) -> i64 { 147 // Don't short-circuit on length -- that leaks. Pad mismatched 148 // lengths into a fixed-size compare instead. 149 var u_ok: i64 = 1 150 if u_got_len != u_want_len { u_ok = 0 } 151 if u_ok == 1 { 152 if ct_memcmp(user_got, user_want, u_got_len) != 0 { 153 u_ok = 0 154 } 155 } 156 var p_ok: i64 = 1 157 if p_got_len != p_want_len { p_ok = 0 } 158 if p_ok == 1 { 159 if ct_memcmp(pass_got, pass_want, p_got_len) != 0 { 160 p_ok = 0 161 } 162 } 163 return u_ok & p_ok 164} 165 166// Compile-only smoke. 167func main() -> i64 { 168 let out: *u8 = sys_mmap(64) 169 let n: i64 = basic_auth_encode("Aladdin", 7, "open sesame", 11, 170 out, 64) 171 // Expected: \"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\" 172 // 6 + 32 = 38 bytes. 173 if n != 38 { return 1 } 174 if out[0] != 0x42 { return 2 } // 'B' 175 if out[6] != 0x51 { return 3 } // 'Q' start of b64 176 177 // Round trip. 178 let u: *u8 = sys_mmap(64) 179 let p: *u8 = sys_mmap(64) 180 let u_len: *i64 = (sys_mmap(16)) as *i64 181 let p_len: *i64 = (sys_mmap(16)) as *i64 182 if basic_auth_decode(out, n, u, 64, u_len, p, 64, p_len) != 0 { 183 return 4 184 } 185 if *u_len != 7 { return 5 } 186 if *p_len != 11 { return 6 } 187 if u[0] != 0x41 { return 7 } // 'A' 188 if p[0] != 0x6F { return 8 } // 'o' 189 190 // Verify match. 191 if basic_auth_verify(u, *u_len, p, *p_len, 192 "Aladdin", 7, "open sesame", 11) != 1 { 193 return 9 194 } 195 // Wrong password. 196 if basic_auth_verify(u, *u_len, p, *p_len, 197 "Aladdin", 7, "open sesamx", 11) != 0 { 198 return 10 199 } 200 return 0 201}