/src/dovecot/src/lib-auth/auth-gs2.c
Line | Count | Source |
1 | | /* Copyright (c) 2023 Dovecot authors, see the included COPYING file */ |
2 | | |
3 | | #include "lib.h" |
4 | | #include "str.h" |
5 | | |
6 | | #include "auth-gs2.h" |
7 | | |
8 | | static const unsigned char auth_gs2_cb_name_char_mask = (1<<0); |
9 | | |
10 | | static const unsigned char auth_gs2_char_lookup[256] = { |
11 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00 |
12 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 |
13 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, // 20 |
14 | | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 30 |
15 | | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 40 |
16 | | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 50 |
17 | | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 60 |
18 | | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 70 |
19 | | |
20 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 |
21 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90 |
22 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A0 |
23 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B0 |
24 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C0 |
25 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D0 |
26 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E0 |
27 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F0 |
28 | | }; |
29 | | |
30 | 7.84k | static inline bool auth_gs2_char_is_cb_name(unsigned char ch) { |
31 | 7.84k | return ((auth_gs2_char_lookup[ch] & auth_gs2_cb_name_char_mask) != 0); |
32 | 7.84k | } |
33 | | |
34 | | static inline const char *_char_sanitize(unsigned char c) |
35 | 23 | { |
36 | 23 | if (c >= 0x20 && c < 0x7F) |
37 | 11 | return t_strdup_printf("'%c'", c); |
38 | 12 | return t_strdup_printf("<0x%02x>", c); |
39 | 23 | } |
40 | | |
41 | | /* RFC 5801, Section 4: |
42 | | |
43 | | The "gs2-authzid" holds the SASL authorization identity. It is encoded using |
44 | | UTF-8 [RFC3629] with three exceptions: |
45 | | |
46 | | o The NUL character is forbidden as required by section 3.4.1 of [RFC4422]. |
47 | | |
48 | | o The server MUST replace any "," (comma) in the string with "=2C". |
49 | | |
50 | | o The server MUST replace any "=" (equals) in the string with "=3D". |
51 | | */ |
52 | | |
53 | | void auth_gs2_encode_username(const char *in, buffer_t *out) |
54 | 3.01k | { |
55 | 53.0M | for (; *in != '\0'; in++) { |
56 | 53.0M | if (in[0] == ',') |
57 | 39.3M | str_append(out, "=2C"); |
58 | 13.6M | else if (in[0] == '=') |
59 | 423k | str_append(out, "=3D"); |
60 | 13.2M | else |
61 | 13.2M | str_append_c(out, *in); |
62 | 53.0M | } |
63 | 3.01k | } |
64 | | |
65 | | int auth_gs2_decode_username(const unsigned char *in, size_t in_size, |
66 | | const char **out_r) |
67 | 2.53k | { |
68 | 2.53k | const unsigned char *p = in, *pend = in + in_size; |
69 | 2.53k | string_t *out; |
70 | | |
71 | 2.53k | out = t_str_new(64); |
72 | 1.97M | while (p < pend) { |
73 | 1.97M | if (*p == '\0' || *p == ',') |
74 | 0 | return -1; |
75 | 1.97M | if (*p == '=') { |
76 | 1.49M | p++; |
77 | 1.49M | if (p >= pend) |
78 | 3 | return -1; |
79 | 1.49M | if (*p == '2') { |
80 | 1.41M | p++; |
81 | 1.41M | if (p >= pend) |
82 | 2 | return -1; |
83 | 1.41M | if (*p != 'C') |
84 | 8 | return -1; |
85 | 1.41M | str_append_c(out, ','); |
86 | 1.41M | } else if (*p == '3') { |
87 | 78.1k | p++; |
88 | 78.1k | if (p >= pend) |
89 | 1 | return -1; |
90 | 78.1k | if (*p != 'D') |
91 | 7 | return -1; |
92 | 78.0k | str_append_c(out, '='); |
93 | 78.0k | } else { |
94 | 19 | return -1; |
95 | 19 | } |
96 | 1.49M | } else { |
97 | 480k | str_append_c(out, *p); |
98 | 480k | } |
99 | 1.97M | p++; |
100 | 1.97M | } |
101 | 2.49k | *out_r = str_c(out); |
102 | 2.49k | return 0; |
103 | 2.53k | } |
104 | | |
105 | | /* RFC 5801, Section 4: |
106 | | |
107 | | UTF8-1-safe = %x01-2B / %x2D-3C / %x3E-7F |
108 | | ;; As UTF8-1 in RFC 3629 except |
109 | | ;; NUL, "=", and ",". |
110 | | UTF8-2 = <as defined in RFC 3629 (STD 63)> |
111 | | UTF8-3 = <as defined in RFC 3629 (STD 63)> |
112 | | UTF8-4 = <as defined in RFC 3629 (STD 63)> |
113 | | UTF8-char-safe = UTF8-1-safe / UTF8-2 / UTF8-3 / UTF8-4 |
114 | | |
115 | | saslname = 1*(UTF8-char-safe / "=2C" / "=3D") |
116 | | gs2-authzid = "a=" saslname |
117 | | ;; GS2 has to transport an authzid since |
118 | | ;; the GSS-API has no equivalent |
119 | | gs2-nonstd-flag = "F" |
120 | | ;; "F" means the mechanism is not a |
121 | | ;; standard GSS-API mechanism in that the |
122 | | ;; RFC 2743, Section 3.1 header was missing |
123 | | cb-name = 1*(ALPHA / DIGIT / "." / "-") |
124 | | ;; See RFC 5056, Section 7. |
125 | | gs2-cb-flag = ("p=" cb-name) / "n" / "y" |
126 | | ;; GS2 channel binding (CB) flag |
127 | | ;; "p" -> client supports and used CB |
128 | | ;; "n" -> client does not support CB |
129 | | ;; "y" -> client supports CB, thinks the server |
130 | | ;; does not |
131 | | gs2-header = [gs2-nonstd-flag ","] gs2-cb-flag "," [gs2-authzid] "," |
132 | | ;; The GS2 header is gs2-header. |
133 | | */ |
134 | | |
135 | | void auth_gs2_header_encode(const struct auth_gs2_header *hdr, buffer_t *out) |
136 | 2.11k | { |
137 | | /* [gs2-nonstd-flag ","] */ |
138 | 2.11k | if (hdr->nonstd) |
139 | 0 | str_append(out, "F,"); |
140 | | |
141 | | /* gs2-cb-flag "," */ |
142 | 2.11k | switch (hdr->cbind.status) { |
143 | 1.37k | case AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT: |
144 | 1.37k | str_append_c(out, 'n'); |
145 | 1.37k | break; |
146 | 0 | case AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT: |
147 | 0 | str_append_c(out, 'y'); |
148 | 0 | break; |
149 | 737 | case AUTH_GS2_CBIND_STATUS_PROVIDED: |
150 | 737 | i_assert(hdr->cbind.name != NULL && *hdr->cbind.name != '\0'); |
151 | 737 | str_append(out, "p="); |
152 | 737 | str_append(out, hdr->cbind.name); |
153 | 737 | break; |
154 | 2.11k | }; |
155 | 2.11k | str_append_c(out, ','); |
156 | | |
157 | | /* [gs2-authzid] "," */ |
158 | 2.11k | if (hdr->authzid != NULL && *hdr->authzid != '\0') { |
159 | 1.43k | str_append(out, "a="); |
160 | 1.43k | auth_gs2_encode_username(hdr->authzid, out); |
161 | 1.43k | } |
162 | 2.11k | str_append_c(out, ','); |
163 | 2.11k | } |
164 | | |
165 | | int auth_gs2_header_decode(const unsigned char *data, size_t size, |
166 | | bool expect_nonstd, struct auth_gs2_header *hdr_r, |
167 | | const unsigned char **hdr_end_r, |
168 | | const char **error_r) |
169 | 1.92k | { |
170 | 1.92k | if (size < 3) { |
171 | 15 | *error_r = "Message too small for GS2 header"; |
172 | 15 | return -1; |
173 | 15 | } |
174 | | |
175 | 1.90k | const unsigned char *p = data, *pend = data + size, *offset; |
176 | 1.90k | struct auth_gs2_header hdr; |
177 | | |
178 | 1.90k | i_zero(&hdr); |
179 | | |
180 | | /* [gs2-nonstd-flag ","] */ |
181 | 1.90k | if (*p == 'F') { |
182 | 1 | if (!expect_nonstd) { |
183 | 1 | *error_r = "Unexpected nonstd 'F' flag"; |
184 | 1 | return -1; |
185 | 1 | } |
186 | 0 | p++; |
187 | 0 | if (*p != ',') { |
188 | 0 | *error_r = "Missing ',' after nonstd 'F' flag"; |
189 | 0 | return -1; |
190 | 0 | } |
191 | 0 | hdr.nonstd = TRUE; |
192 | 0 | p++; |
193 | 0 | } |
194 | | |
195 | | /* gs2-cb-flag "," */ |
196 | 1.90k | switch (*p) { |
197 | 1.21k | case 'n': |
198 | 1.21k | hdr.cbind.status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT; |
199 | 1.21k | break; |
200 | 2 | case 'y': |
201 | 2 | hdr.cbind.status = AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT; |
202 | 2 | break; |
203 | 661 | case 'p': |
204 | 661 | hdr.cbind.status = AUTH_GS2_CBIND_STATUS_PROVIDED; |
205 | 661 | break; |
206 | 23 | default: |
207 | 23 | *error_r = t_strdup_printf( |
208 | 23 | "Invalid channel bind flag %s", |
209 | 23 | _char_sanitize(*p)); |
210 | 23 | return -1; |
211 | 1.90k | } |
212 | 1.88k | p++; |
213 | 1.88k | if (hdr.cbind.status == AUTH_GS2_CBIND_STATUS_PROVIDED) { |
214 | | /* "=" cb-name */ |
215 | 661 | if (p >= pend || *p != '=') { |
216 | 4 | *error_r = "Missing '=' after 'p' flag"; |
217 | 4 | return -1; |
218 | 4 | } |
219 | 657 | p++; |
220 | | |
221 | 657 | offset = p; |
222 | 657 | if (p >= pend || *p == ',') { |
223 | 1 | *error_r = "Empty channel bind name"; |
224 | 1 | return -1; |
225 | 1 | } |
226 | 8.49k | while (p < pend && *p != ',') { |
227 | 7.84k | if (!auth_gs2_char_is_cb_name(*p)) { |
228 | 13 | *error_r = "Invalid channel bind name"; |
229 | 13 | return -1; |
230 | 13 | } |
231 | 7.83k | p++; |
232 | 7.83k | } |
233 | 643 | hdr.cbind.name = t_strdup_until(offset, p); |
234 | 643 | } |
235 | 1.86k | if (p >= pend || *p != ',') { |
236 | 12 | *error_r = "Missing ',' after channel bind flag"; |
237 | 12 | return -1; |
238 | 12 | } |
239 | 1.85k | p++; |
240 | | |
241 | | /* [gs2-authzid] "," */ |
242 | 1.85k | if (p < pend && *p == 'a') { |
243 | 1.19k | p++; |
244 | 1.19k | if (p >= pend || *p != '=') { |
245 | 11 | *error_r = "Missing '=' after 'a'"; |
246 | 11 | return -1; |
247 | 11 | } |
248 | 1.18k | p++; |
249 | | |
250 | 1.18k | offset = p; |
251 | 1.18k | if (p >= pend || *p == ',') { |
252 | 2 | *error_r = "Empty authzid field"; |
253 | 2 | return -1; |
254 | 2 | } |
255 | 4.56M | while (p < pend && *p != ',') |
256 | 4.56M | p++; |
257 | 1.18k | if (auth_gs2_decode_username(offset, p - offset, |
258 | 1.18k | &hdr.authzid) < 0) { |
259 | 24 | *error_r = "Invalid authzid field"; |
260 | 24 | return -1; |
261 | 24 | } |
262 | 1.18k | } |
263 | 1.81k | if (p >= pend || *p != ',') { |
264 | 11 | *error_r = "Missing ',' after authzid field"; |
265 | 11 | return -1; |
266 | 11 | } |
267 | 1.80k | p++; |
268 | | |
269 | 1.80k | *error_r = NULL; |
270 | 1.80k | *hdr_r = hdr; |
271 | 1.80k | *hdr_end_r = p; |
272 | 1.80k | return 0; |
273 | 1.81k | } |