code wiki / (root) / basic_auth.nx

basic_auth.nx source

↩ module page · 195 lines · 6533 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 24import "syscalls.nx" 25import "base64.nx" 26import "ct.nx" 27 28const BA_ERR_FORMAT: i64 = -1 29const BA_ERR_SHORT: i64 = -2 30 31// Encode username + password into an Authorization header VALUE 32// (the part after the header name, e.g. \"Basic dXNlcjpwYXNz\"). 33// Returns bytes written or BA_ERR_SHORT. 34func basic_auth_encode(username: *u8, u_len: i64, 35 password: *u8, p_len: i64, 36 out: *u8, cap: i64) -> i64 { 37 // First write \"Basic \". 38 let prefix_len: i64 = 6 39 if cap < prefix_len { return BA_ERR_SHORT } 40 out[0] = 0x42 // 'B' 41 out[1] = 0x61 // 'a' 42 out[2] = 0x73 // 's' 43 out[3] = 0x69 // 'i' 44 out[4] = 0x63 // 'c' 45 out[5] = 0x20 // ' ' 46 47 // Build \"user:pass\" in scratch. 48 let scratch_len: i64 = u_len + 1 + p_len 49 let scratch: *u8 = sys_mmap(scratch_len + 16) 50 var i: i64 = 0 51 while i < u_len { 52 scratch[i] = username[i] 53 i = i + 1 54 } 55 scratch[u_len] = 0x3A 56 i = 0 57 while i < p_len { 58 scratch[u_len + 1 + i] = password[i] 59 i = i + 1 60 } 61 62 // Base64 after prefix. 63 let b64_cap: i64 = cap - prefix_len 64 let b64_len: i64 = b64_encode(scratch, scratch_len, out + prefix_len) 65 if b64_len < 0 { return BA_ERR_SHORT } 66 if b64_len > b64_cap { return BA_ERR_SHORT } 67 return prefix_len + b64_len 68} 69 70// Decode an Authorization header value. Writes username to 71// u_out, password to p_out; lengths to u_len_out, p_len_out. 72// Returns 0 on success, negative on failure. 73func basic_auth_decode(hdr: *u8, n: i64, 74 u_out: *u8, u_cap: i64, u_len_out: *i64, 75 p_out: *u8, p_cap: i64, p_len_out: *i64) -> i64 { 76 if n < 7 { return BA_ERR_SHORT } // \"Basic X\" min 77 78 // Match \"Basic \" case-insensitively. 79 let b: i64 = hdr[0] 80 let a: i64 = hdr[1] 81 let s: i64 = hdr[2] 82 let i_: i64 = hdr[3] 83 let c: i64 = hdr[4] 84 let sp: i64 = hdr[5] 85 // Lowercase-compare first five. 86 if (b | 0x20) != 0x62 { return BA_ERR_FORMAT } 87 if (a | 0x20) != 0x61 { return BA_ERR_FORMAT } 88 if (s | 0x20) != 0x73 { return BA_ERR_FORMAT } 89 if (i_ | 0x20) != 0x69 { return BA_ERR_FORMAT } 90 if (c | 0x20) != 0x63 { return BA_ERR_FORMAT } 91 if sp != 0x20 { return BA_ERR_FORMAT } 92 93 // Base64-decode the remainder. 94 let b64_off: i64 = 6 95 let b64_len: i64 = n - 6 96 // Worst-case decoded size = ceil(b64_len * 3 / 4). 97 let scratch_cap: i64 = b64_len + 4 98 let scratch: *u8 = sys_mmap(scratch_cap + 16) 99 let decoded_len: i64 = b64_decode(hdr + b64_off, b64_len, scratch) 100 if decoded_len < 0 { return BA_ERR_FORMAT } 101 102 // Split at first ':'. 103 var colon: i64 = -1 104 var i: i64 = 0 105 while i < decoded_len { 106 if scratch[i] == 0x3A { 107 colon = i 108 break 109 } 110 i = i + 1 111 } 112 if colon < 0 { return BA_ERR_FORMAT } 113 114 let u_len: i64 = colon 115 let p_len: i64 = decoded_len - colon - 1 116 if u_len > u_cap { return BA_ERR_SHORT } 117 if p_len > p_cap { return BA_ERR_SHORT } 118 119 i = 0 120 while i < u_len { 121 u_out[i] = scratch[i] 122 i = i + 1 123 } 124 i = 0 125 while i < p_len { 126 p_out[i] = scratch[colon + 1 + i] 127 i = i + 1 128 } 129 *u_len_out = u_len 130 *p_len_out = p_len 131 return 0 132} 133 134// Constant-time check: does (user_got, pass_got) match (user_want, 135// pass_want)? Returns 1 on match, 0 otherwise. Critical for 136// admin login flows -- naive memcmp leaks timing. 137func basic_auth_verify(user_got: *u8, u_got_len: i64, 138 pass_got: *u8, p_got_len: i64, 139 user_want: *u8, u_want_len: i64, 140 pass_want: *u8, p_want_len: i64) -> i64 { 141 // Don't short-circuit on length -- that leaks. Pad mismatched 142 // lengths into a fixed-size compare instead. 143 var u_ok: i64 = 1 144 if u_got_len != u_want_len { u_ok = 0 } 145 if u_ok == 1 { 146 if ct_memcmp(user_got, user_want, u_got_len) != 0 { 147 u_ok = 0 148 } 149 } 150 var p_ok: i64 = 1 151 if p_got_len != p_want_len { p_ok = 0 } 152 if p_ok == 1 { 153 if ct_memcmp(pass_got, pass_want, p_got_len) != 0 { 154 p_ok = 0 155 } 156 } 157 return u_ok & p_ok 158} 159 160// Compile-only smoke. 161func main() -> i64 { 162 let out: *u8 = sys_mmap(64) 163 let n: i64 = basic_auth_encode("Aladdin", 7, "open sesame", 11, 164 out, 64) 165 // Expected: \"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\" 166 // 6 + 32 = 38 bytes. 167 if n != 38 { return 1 } 168 if out[0] != 0x42 { return 2 } // 'B' 169 if out[6] != 0x51 { return 3 } // 'Q' start of b64 170 171 // Round trip. 172 let u: *u8 = sys_mmap(64) 173 let p: *u8 = sys_mmap(64) 174 let u_len: *i64 = (sys_mmap(16)) as *i64 175 let p_len: *i64 = (sys_mmap(16)) as *i64 176 if basic_auth_decode(out, n, u, 64, u_len, p, 64, p_len) != 0 { 177 return 4 178 } 179 if *u_len != 7 { return 5 } 180 if *p_len != 11 { return 6 } 181 if u[0] != 0x41 { return 7 } // 'A' 182 if p[0] != 0x6F { return 8 } // 'o' 183 184 // Verify match. 185 if basic_auth_verify(u, *u_len, p, *p_len, 186 "Aladdin", 7, "open sesame", 11) != 1 { 187 return 9 188 } 189 // Wrong password. 190 if basic_auth_verify(u, *u_len, p, *p_len, 191 "Aladdin", 7, "open sesamx", 11) != 0 { 192 return 10 193 } 194 return 0 195}