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}