]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-auth: Add auth-gs2 API
authorStephan Bosch <stephan.bosch@open-xchange.com>
Wed, 8 Mar 2023 20:31:09 +0000 (21:31 +0100)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Tue, 2 Sep 2025 05:25:53 +0000 (05:25 +0000)
src/lib-auth/Makefile.am
src/lib-auth/auth-gs2.c [new file with mode: 0644]
src/lib-auth/auth-gs2.h [new file with mode: 0644]
src/lib-auth/test-auth-gs2.c [new file with mode: 0644]

index 380457baf6d917834c8c94cf3f2252f31874a944..65a08adcec2fe2fb6d19a4f70e17b2ea8c00b5e6 100644 (file)
@@ -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 (file)
index 0000000..e7ceaf9
--- /dev/null
@@ -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         = <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;
+}
diff --git a/src/lib-auth/auth-gs2.h b/src/lib-auth/auth-gs2.h
new file mode 100644 (file)
index 0000000..9740a8f
--- /dev/null
@@ -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 (file)
index 0000000..2bfd3cb
--- /dev/null
@@ -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);
+}