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}