--- /dev/null
+/* Copyright (c) 2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+
+#include "auth-gs2.h"
+
+static const unsigned char auth_gs2_cb_name_char_mask = (1<<0);
+
+static const unsigned char auth_gs2_char_lookup[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, // 20
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 30
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 40
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 50
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 60
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 70
+
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F0
+};
+
+static inline bool auth_gs2_char_is_cb_name(unsigned char ch) {
+ return ((auth_gs2_char_lookup[ch] & auth_gs2_cb_name_char_mask) != 0);
+}
+
+static inline const char *_char_sanitize(unsigned char c)
+{
+ if (c >= 0x20 && c < 0x7F)
+ return t_strdup_printf("'%c'", c);
+ return t_strdup_printf("<0x%02x>", c);
+}
+
+/* RFC 5801, Section 4:
+
+ The "gs2-authzid" holds the SASL authorization identity. It is encoded using
+ UTF-8 [RFC3629] with three exceptions:
+
+ o The NUL character is forbidden as required by section 3.4.1 of [RFC4422].
+
+ o The server MUST replace any "," (comma) in the string with "=2C".
+
+ o The server MUST replace any "=" (equals) in the string with "=3D".
+ */
+
+void auth_gs2_encode_username(const char *in, buffer_t *out)
+{
+ for (; *in != '\0'; in++) {
+ if (in[0] == ',')
+ str_append(out, "=2C");
+ else if (in[0] == '=')
+ str_append(out, "=3D");
+ else
+ str_append_c(out, *in);
+ }
+}
+
+int auth_gs2_decode_username(const unsigned char *in, size_t in_size,
+ const char **out_r)
+{
+ const unsigned char *p = in, *pend = in + in_size;
+ string_t *out;
+
+ out = t_str_new(64);
+ while (p < pend) {
+ if (*p == '\0' || *p == ',')
+ return -1;
+ if (*p == '=') {
+ p++;
+ if (p >= pend)
+ return -1;
+ if (*p == '2') {
+ p++;
+ if (p >= pend)
+ return -1;
+ if (*p != 'C')
+ return -1;
+ str_append_c(out, ',');
+ } else if (*p == '3') {
+ p++;
+ if (p >= pend)
+ return -1;
+ if (*p != 'D')
+ return -1;
+ str_append_c(out, '=');
+ } else {
+ return -1;
+ }
+ } else {
+ str_append_c(out, *p);
+ }
+ p++;
+ }
+ *out_r = str_c(out);
+ return 0;
+}
+
+/* RFC 5801, Section 4:
+
+ UTF8-1-safe = %x01-2B / %x2D-3C / %x3E-7F
+ ;; As UTF8-1 in RFC 3629 except
+ ;; NUL, "=", and ",".
+ UTF8-2 = <as defined in RFC 3629 (STD 63)>
+ UTF8-3 = <as defined in RFC 3629 (STD 63)>
+ UTF8-4 = <as defined in RFC 3629 (STD 63)>
+ UTF8-char-safe = UTF8-1-safe / UTF8-2 / UTF8-3 / UTF8-4
+
+ saslname = 1*(UTF8-char-safe / "=2C" / "=3D")
+ gs2-authzid = "a=" saslname
+ ;; GS2 has to transport an authzid since
+ ;; the GSS-API has no equivalent
+ gs2-nonstd-flag = "F"
+ ;; "F" means the mechanism is not a
+ ;; standard GSS-API mechanism in that the
+ ;; RFC 2743, Section 3.1 header was missing
+ cb-name = 1*(ALPHA / DIGIT / "." / "-")
+ ;; See RFC 5056, Section 7.
+ gs2-cb-flag = ("p=" cb-name) / "n" / "y"
+ ;; GS2 channel binding (CB) flag
+ ;; "p" -> client supports and used CB
+ ;; "n" -> client does not support CB
+ ;; "y" -> client supports CB, thinks the server
+ ;; does not
+ gs2-header = [gs2-nonstd-flag ","] gs2-cb-flag "," [gs2-authzid] ","
+ ;; The GS2 header is gs2-header.
+ */
+
+void auth_gs2_header_encode(const struct auth_gs2_header *hdr, buffer_t *out)
+{
+ /* [gs2-nonstd-flag ","] */
+ if (hdr->nonstd)
+ str_append(out, "F,");
+
+ /* gs2-cb-flag "," */
+ switch (hdr->cbind.status) {
+ case AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT:
+ str_append_c(out, 'n');
+ break;
+ case AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT:
+ str_append_c(out, 'y');
+ break;
+ case AUTH_GS2_CBIND_STATUS_PROVIDED:
+ i_assert(hdr->cbind.name != NULL && *hdr->cbind.name != '\0');
+ str_append(out, "p=");
+ str_append(out, hdr->cbind.name);
+ break;
+ };
+ str_append_c(out, ',');
+
+ /* [gs2-authzid] "," */
+ if (hdr->authzid != NULL && *hdr->authzid != '\0') {
+ str_append(out, "a=");
+ auth_gs2_encode_username(hdr->authzid, out);
+ }
+ str_append_c(out, ',');
+}
+
+int auth_gs2_header_decode(const unsigned char *data, size_t size,
+ bool expect_nonstd, struct auth_gs2_header *hdr_r,
+ const unsigned char **hdr_end_r,
+ const char **error_r)
+{
+ if (size < 3) {
+ *error_r = "Message too small for GS2 header";
+ return -1;
+ }
+
+ const unsigned char *p = data, *pend = data + size, *offset;
+ struct auth_gs2_header hdr;
+
+ i_zero(&hdr);
+
+ /* [gs2-nonstd-flag ","] */
+ if (*p == 'F') {
+ if (!expect_nonstd) {
+ *error_r = "Unexpected nonstd 'F' flag";
+ return -1;
+ }
+ p++;
+ if (*p != ',') {
+ *error_r = "Missing ',' after nonstd 'F' flag";
+ return -1;
+ }
+ hdr.nonstd = TRUE;
+ p++;
+ }
+
+ /* gs2-cb-flag "," */
+ switch (*p) {
+ case 'n':
+ hdr.cbind.status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT;
+ break;
+ case 'y':
+ hdr.cbind.status = AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT;
+ break;
+ case 'p':
+ hdr.cbind.status = AUTH_GS2_CBIND_STATUS_PROVIDED;
+ break;
+ default:
+ *error_r = t_strdup_printf(
+ "Invalid channel bind flag %s",
+ _char_sanitize(*p));
+ return -1;
+ }
+ p++;
+ if (hdr.cbind.status == AUTH_GS2_CBIND_STATUS_PROVIDED) {
+ /* "=" cb-name */
+ if (p >= pend || *p != '=') {
+ *error_r = "Missing '=' after 'p' flag";
+ return -1;
+ }
+ p++;
+
+ offset = p;
+ if (p >= pend || *p == ',') {
+ *error_r = "Empty channel bind name";
+ return -1;
+ }
+ while (p < pend && *p != ',') {
+ if (!auth_gs2_char_is_cb_name(*p)) {
+ *error_r = "Invalid channel bind name";
+ return -1;
+ }
+ p++;
+ }
+ hdr.cbind.name = t_strdup_until(offset, p);
+ }
+ if (p >= pend || *p != ',') {
+ *error_r = "Missing ',' after channel bind flag";
+ return -1;
+ }
+ p++;
+
+ /* [gs2-authzid] "," */
+ if (p < pend && *p == 'a') {
+ p++;
+ if (p >= pend || *p != '=') {
+ *error_r = "Missing '=' after 'a'";
+ return -1;
+ }
+ p++;
+
+ offset = p;
+ if (p >= pend || *p == ',') {
+ *error_r = "Empty authzid field";
+ return -1;
+ }
+ while (p < pend && *p != ',')
+ p++;
+ if (auth_gs2_decode_username(offset, p - offset,
+ &hdr.authzid) < 0) {
+ *error_r = "Invalid authzid field";
+ return -1;
+ }
+ }
+ if (p >= pend || *p != ',') {
+ *error_r = "Missing ',' after authzid field";
+ return -1;
+ }
+ p++;
+
+ *error_r = NULL;
+ *hdr_r = hdr;
+ *hdr_end_r = p;
+ return 0;
+}
--- /dev/null
+/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "auth-gs2.h"
+
+struct test_gs2_header_valid {
+ const char *in;
+
+ struct auth_gs2_header hdr;
+ size_t hdr_len;
+
+ bool expect_nonstd;
+};
+
+static const struct test_gs2_header_valid gs2_header_valid_tests[] = {
+ {
+ .in = "n,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ },
+ },
+ {
+ .in = "y,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT,
+ },
+ },
+ },
+ {
+ .in = "p=frop,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "frop",
+ },
+ },
+ },
+ {
+ .in = "p=tls-exporter,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "tls-exporter",
+ },
+ },
+ },
+ {
+ .in = "p=frop2,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "frop2",
+ },
+ },
+ },
+ {
+ .in = "p=vnd.example.com-frop,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "vnd.example.com-frop",
+ },
+ },
+ },
+ {
+ .in = "n,a=frop,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = "frop",
+ },
+ },
+ {
+ .in = "y,a=frop,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT,
+ },
+ .authzid = "frop",
+ },
+ },
+ {
+ .in = "p=frop,a=frop,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "frop",
+ },
+ .authzid = "frop",
+ },
+ },
+ {
+ .in = "n,a=frop=2Cfriep=3Dfrml,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = "frop,friep=frml",
+ },
+ },
+ {
+ .in = "n,a==2Cfrop=2C,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = ",frop,",
+ },
+ },
+ {
+ .in = "n,a==3Dfrop=3D,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = "=frop=",
+ },
+ },
+ {
+ .in = "n,a==2C=3D,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = ",=",
+ },
+ },
+ {
+ .in = "n,a==2C=3D=2C=3D=2C=3D,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = ",=,=,=",
+ },
+ },
+ {
+ .in = "n,a==2C=2C=2C=3D=3D=3D,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = ",,,===",
+ },
+ },
+ {
+ .in = "n,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ },
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "y,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT,
+ },
+ },
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "p=frop,,",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "frop",
+ },
+ },
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,n,,",
+ .hdr = {
+ .nonstd = TRUE,
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ },
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,y,,",
+ .hdr = {
+ .nonstd = TRUE,
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT,
+ },
+ },
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,p=frop,,",
+ .hdr = {
+ .nonstd = TRUE,
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_PROVIDED,
+ .name = "frop",
+ },
+ },
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "n,a=frop=2Cfriep=3Dfrml,n=user",
+ .hdr = {
+ .cbind = {
+ .status = AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT,
+ },
+ .authzid = "frop,friep=frml",
+ },
+ .hdr_len = 24,
+ },
+ {
+ .in = "n,a=fr,p,",
+ .hdr = {
+ .authzid = "fr",
+ },
+ .hdr_len = 7,
+ },
+};
+
+static void test_gs2_header_valid(void)
+{
+ unsigned int i;
+ buffer_t *buf;
+ int ret;
+
+ buf = t_buffer_create(128);
+ for (i = 0; i < N_ELEMENTS(gs2_header_valid_tests); i++) {
+ const struct test_gs2_header_valid *test =
+ &gs2_header_valid_tests[i];
+ size_t test_hdr_len = (test->hdr_len == 0 ?
+ strlen(test->in) : test->hdr_len);
+ struct auth_gs2_header hdr;
+ const unsigned char *hdr_end = NULL;
+ const char *error;
+
+ test_begin(t_strdup_printf("auth gs2 header valid [%u]",
+ i + 1));
+
+ ret = auth_gs2_header_decode((unsigned char *)test->in,
+ strlen(test->in),
+ test->expect_nonstd, &hdr,
+ &hdr_end, &error);
+ test_out_reason("decode success", ret >= 0, error);
+ if (ret < 0)
+ continue;
+
+ test_assert(hdr.cbind.status == test->hdr.cbind.status);
+ test_assert_strcmp(hdr.cbind.name, test->hdr.cbind.name);
+ test_assert_strcmp(hdr.authzid, test->hdr.authzid);
+ test_assert(hdr.nonstd == test->hdr.nonstd);
+ test_assert(hdr_end ==
+ (unsigned char *)(test->in + test_hdr_len));
+
+ auth_gs2_header_encode(&hdr, buf);
+
+ test_assert_strcmp(t_strndup(test->in, test_hdr_len),
+ str_c(buf));
+
+ test_end();
+ buffer_clear(buf);
+ }
+
+}
+
+struct test_gs2_header_invalid {
+ const char *in;
+ size_t nul_at;
+
+ bool expect_nonstd;
+};
+
+static const struct test_gs2_header_invalid gs2_header_invalid_tests[] = {
+ {
+ .in = "",
+ },
+ {
+ .in = ",",
+ },
+ {
+ .in = ",,",
+ },
+ {
+ .in = "F,n",
+ },
+ {
+ .in = "F,n",
+ .nul_at = 2,
+ },
+ {
+ .in = "F,n",
+ .nul_at = 3,
+ },
+ {
+ .in = "F,n",
+ .nul_at = 2,
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,n",
+ .nul_at = 3,
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,n,",
+ },
+ {
+ .in = "F,n,,",
+ },
+ {
+ .in = "Fn,",
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "Fn,,",
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "q,,",
+ },
+ {
+ .in = "F,q",
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,q,",
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "F,q,,",
+ .expect_nonstd = TRUE,
+ },
+ {
+ .in = "nn,",
+ },
+ {
+ .in = "n,,",
+ .nul_at = 1,
+ },
+ {
+ .in = "n,,",
+ .nul_at = 2,
+ },
+ {
+ .in = "p,,",
+ },
+ {
+ .in = "p=,",
+ },
+ {
+ .in = "p=,,",
+ },
+ {
+ .in = "p=_frop,,",
+ },
+ {
+ .in = "p=frop_,,",
+ },
+ {
+ .in = "p=fr_p,,",
+ },
+ {
+ .in = "p=frop,,",
+ .nul_at = 5,
+ },
+ {
+ .in = "p=frop,,",
+ .nul_at = 3,
+ },
+ {
+ .in = "p=frop,,",
+ .nul_at = 6,
+ },
+ {
+ .in = "p=frop,,",
+ .nul_at = 7,
+ },
+ {
+ .in = "p=frop",
+ },
+ {
+ .in = "n,,",
+ .nul_at = 3,
+ },
+ {
+ .in = "n,a",
+ },
+ {
+ .in = "n,a,",
+ },
+ {
+ .in = "n,a=",
+ },
+ {
+ .in = "n,a=,",
+ },
+ {
+ .in = "n,a=frop,",
+ .nul_at = 7,
+ },
+ {
+ .in = "n,a=frop,",
+ .nul_at = 5,
+ },
+ {
+ .in = "n,a=frop,",
+ .nul_at = 8,
+ },
+ {
+ .in = "n,a=frop,",
+ .nul_at = 9,
+ },
+ {
+ .in = "n,a=fr=p,",
+ },
+ {
+ .in = "n,a==rop,",
+ },
+ {
+ .in = "n,a=fro=,",
+ },
+ {
+ .in = "n,a=fr=20p,",
+ },
+ {
+ .in = "n,a==20rop,",
+ },
+ {
+ .in = "n,a=fro=20,",
+ },
+ {
+ .in = "n,a=fr=32p,",
+ },
+ {
+ .in = "n,a==32rop,",
+ },
+ {
+ .in = "n,a=fro=32,",
+ },
+ {
+ .in = "n,a=frop",
+ },
+ {
+ .in = "p=frop,",
+ },
+};
+
+static void test_gs2_header_invalid(void)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < N_ELEMENTS(gs2_header_invalid_tests); i++) {
+ const struct test_gs2_header_invalid *test =
+ &gs2_header_invalid_tests[i];
+ const unsigned char *test_hdr = (unsigned char *)test->in;
+ size_t test_hdr_len = strlen(test->in);
+ struct auth_gs2_header hdr;
+ const unsigned char *hdr_end = NULL;
+ const char *error;
+
+ test_begin(t_strdup_printf("auth gs2 header invalid [%u]",
+ i + 1));
+
+ if (test->nul_at > 0) {
+ unsigned char *test_hdr_nul;
+
+ i_assert((test->nul_at - 1) < test_hdr_len);
+ test_hdr_nul =
+ (unsigned char *)t_strdup_noconst(test->in);
+ test_hdr_nul[test->nul_at - 1] = '\0';
+ test_hdr = test_hdr_nul;
+ }
+
+ ret = auth_gs2_header_decode(test_hdr, test_hdr_len,
+ test->expect_nonstd, &hdr,
+ &hdr_end, &error);
+ test_out_reason("decode failure", ret < 0, error);
+
+ test_end();
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_gs2_header_valid,
+ test_gs2_header_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}