From: Stephan Bosch Date: Wed, 8 Mar 2023 20:31:09 +0000 (+0100) Subject: lib-auth: Add auth-gs2 API X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=52a779f969934ee2a5786e632304e7aade678d5c;p=thirdparty%2Fdovecot%2Fcore.git lib-auth: Add auth-gs2 API --- diff --git a/src/lib-auth/Makefile.am b/src/lib-auth/Makefile.am index 380457baf6..65a08adcec 100644 --- a/src/lib-auth/Makefile.am +++ b/src/lib-auth/Makefile.am @@ -7,6 +7,7 @@ AM_CPPFLAGS = \ -I$(top_srcdir)/src/lib-otp libauth_la_SOURCES = \ + auth-gs2.c \ auth-scram.c \ auth-scram-client.c \ auth-scram-server.c \ @@ -28,6 +29,7 @@ libauth_crypt_la_LIBADD = \ headers = \ mycrypt.h \ + auth-gs2.h \ auth-scram.h \ auth-scram-client.h \ auth-scram-server.h \ @@ -41,6 +43,7 @@ noinst_HEADERS = crypt-blowfish.h \ test_programs = \ test-password-scheme \ + test-auth-gs2 \ test-auth-scram noinst_PROGRAMS = $(test_programs) @@ -68,6 +71,14 @@ test_password_scheme_DEPENDENCIES = \ ../lib-otp/libotp.la \ $(test_deps) +test_auth_gs2_SOURCES = \ + test-auth-gs2.c +test_auth_gs2_LDFLAGS = -export-dynamic +test_auth_gs2_LDADD = \ + $(test_libs) +test_auth_gs2_DEPENDENCIES = \ + $(test_deps) + test_auth_scram_SOURCES = \ test-auth-scram.c test_auth_scram_LDFLAGS = -export-dynamic diff --git a/src/lib-auth/auth-gs2.c b/src/lib-auth/auth-gs2.c new file mode 100644 index 0000000000..e7ceaf9eb6 --- /dev/null +++ b/src/lib-auth/auth-gs2.c @@ -0,0 +1,273 @@ +/* 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 = + UTF8-3 = + UTF8-4 = + 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; +} diff --git a/src/lib-auth/auth-gs2.h b/src/lib-auth/auth-gs2.h new file mode 100644 index 0000000000..9740a8f7f2 --- /dev/null +++ b/src/lib-auth/auth-gs2.h @@ -0,0 +1,31 @@ +#ifndef AUTH_GS2_H +#define AUTH_GS2_H + +enum auth_gs2_cbind_status { + AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT = 0, + AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT, + AUTH_GS2_CBIND_STATUS_PROVIDED, +}; + +struct auth_gs2_header { + struct { + enum auth_gs2_cbind_status status; + const char *name; + } cbind; + + const char *authzid; + + bool nonstd:1; +}; + +void auth_gs2_encode_username(const char *in, buffer_t *out); +int auth_gs2_decode_username(const unsigned char *in, size_t in_size, + const char **out_r); + +void auth_gs2_header_encode(const struct auth_gs2_header *hdr, buffer_t *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); + +#endif diff --git a/src/lib-auth/test-auth-gs2.c b/src/lib-auth/test-auth-gs2.c new file mode 100644 index 0000000000..2bfd3cb508 --- /dev/null +++ b/src/lib-auth/test-auth-gs2.c @@ -0,0 +1,500 @@ +/* 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); +}