diff --git a/hasl/haslgs2header.h b/hasl/haslgs2header.h
new file mode 100644
--- /dev/null
+++ b/hasl/haslgs2header.h
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 Hasl Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef HASL_GS2_HEADER_H
+#define HASL_GS2_HEADER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * HaslGs2HeaderChannelBinding:
+ * @HaslGs2HeaderChannelBindingClientNotSupported: The client doesn't support
+ *                                                 channel binding.
+ * @HaslGs2HeaderChannelBindingClientSupported: The client supports channel
+ *                                              binding and has specified a
+ *                                              name.
+ * @HaslGs2HeaderChannelBindingClientSupportedServerUnknown: The client
+ *                                                           supports
+ *                                                           channel binding,
+ *                                                           doesn't know if
+ *                                                           the server does.
+ *
+ * This enum tells the header about channel bindings for this connection. See
+ * https://datatracker.ietf.org/doc/html/rfc5801#section-4 for more
+ * information.
+ *
+ * Since: 0.3.0
+ */
+typedef enum {
+	HaslGs2HeaderChannelBindingClientNotSupported,
+	HaslGs2HeaderChannelBindingClientSupported,
+	HaslGs2HeaderChannelBindingClientSupportedServerUnknown,
+} HaslGs2HeaderChannelBinding;
+
+#define HASL_GS2_HEADER_DOMAIN (g_quark_from_static_string("hash-gs2-header"))
+
+/**
+ * HaslGs2Header:
+ *
+ * This is an object to make it easier to generate a GS2 Header. See
+ * https://www.rfc-editor.org/rfc/rfc5801#section-4 for the formal definition.
+ *
+ * This is primarily meant to be used by mechanism implementations and should
+ * not be needed for normal use cases.
+ *
+ * Since: 0.3.0
+ */
+#define HASL_TYPE_GS2_HEADER (hasl_gs2_header_get_type())
+G_DECLARE_FINAL_TYPE(HaslGs2Header, hasl_gs2_header, HASL, GS2_HEADER, GObject)
+
+/**
+ * hasl_gs2_header_new:
+ *
+ * Creates a new gs2_header.
+ *
+ * Returns: (transfer full): The new gs2_header.
+ *
+ * Since: 0.3.0
+ */
+HaslGs2Header *hasl_gs2_header_new(void);
+
+/**
+ * hasl_gs2_header_to_string:
+ * @header: The instance.
+ * @error: (out): A return address for an error.
+ *
+ * Serializes @header to a string in the correct format.
+ *
+ * Returns: (transfer full): The serialized header.
+ *
+ * Since: 0.3.0
+ */
+char *hasl_gs2_header_to_string(HaslGs2Header *header, GError **error);
+
+/**
+ * hasl_gs2_header_get_standard_mechanism:
+ * @header: The instance.
+ *
+ * Gets whether or not this header is for a standard GSS-API mechanism.
+ *
+ * Returns: %TRUE if this mechanism is standard, %FALSE otherwise.
+ *
+ * Since: 0.3.0
+ */
+gboolean hasl_gs2_header_get_standard_mechanism(HaslGs2Header *header);
+
+/**
+ * hasl_gs2_header_set_standard_mechanism:
+ * @header: The instance.
+ * @standard: The new value.
+ *
+ * Sets whether or not the mechanism is a standard GSS-API mechanism.
+ *
+ * Since: 0.3.0
+ */
+void hasl_gs2_header_set_standard_mechanism(HaslGs2Header *header, gboolean standard);
+
+/**
+ * hasl_gs2_header_get_channel_binding:
+ * @header: The instance.
+ *
+ * Gets the channel binding value for this header.
+ *
+ * Returns: The channel binding value.
+ *
+ * Since: 0.3.0
+ */
+HaslGs2HeaderChannelBinding hasl_gs2_header_get_channel_binding(HaslGs2Header *header);
+
+/**
+ * hasl_gs2_header_set_channel_binding:
+ * @header: The instance.
+ * @channel_binding: The channel binding flag.
+ *
+ * Sets the channel binding flag for @header.
+ *
+ * Since: 0.3.0
+ */
+void hasl_gs2_header_set_channel_binding(HaslGs2Header *header, HaslGs2HeaderChannelBinding channel_binding);
+
+/**
+ * hasl_gs2_header_get_channel_binding_name:
+ * @header: The instance.
+ *
+ * Gets the name of the channel binding that is in use.
+ *
+ * Returns: (nullable): The channel binding name.
+ *
+ * Since: 0.3.0
+ */
+const char *hasl_gs2_header_get_channel_binding_name(HaslGs2Header *header);
+
+/**
+ * hasl_gs2_header_set_channel_binding_name:
+ * @header: The instance.
+ * @name: (nullable): The name of the channel binding data.
+ *
+ * Sets the name of the channel binding data that should be used.
+ *
+ * Since: 0.3.0
+ */
+void hasl_gs2_header_set_channel_binding_name(HaslGs2Header *header, const char *name);
+
+/**
+ * hasl_gs2_header_get_authzid:
+ * @header: The instance.
+ *
+ * Gets the authzid to use for this header.
+ *
+ * Returns: (nullable): The authzid for this header.
+ *
+ * Since: 0.3.0
+ */
+const char *hasl_gs2_header_get_authzid(HaslGs2Header *header);
+
+/**
+ * hasl_gs2_header_set_authzid:
+ * @header: The instance.
+ * @authzid: (nullable): The username to authenticate for.
+ *
+ * Sets the username to authenticate for. This is used when an administrator
+ * or moderator needs to authenticate as a specific user using their own
+ * credentials.
+ *
+ * Since: 0.3.0
+ */
+void hasl_gs2_header_set_authzid(HaslGs2Header *header, const char *authzid);
+
+G_END_DECLS
+
+#endif /* HASL_GS2_HEADER_H */
# HG changeset patch
# Node ID f0f9a469115d
# Parent  d24109384bda
diff --git a/hasl/haslgs2header.c b/hasl/haslgs2header.c
new file mode 100644
--- /dev/null
+++ b/hasl/haslgs2header.c
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2023 Hasl Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include <stringprep.h>
+
+#include "haslgs2header.h"
+
+#include "haslenums.h"
+
+enum {
+	PROP_0,
+	PROP_STANDARD_MECHANISM,
+	PROP_CHANNEL_BINDING,
+	PROP_CHANNEL_BINDING_NAME,
+	PROP_AUTHZID,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+struct _HaslGs2Header {
+	GObject parent;
+
+	gboolean standard_mechanism;
+
+	HaslGs2HeaderChannelBinding channel_binding;
+	char *channel_binding_name;
+
+	char *authzid;
+};
+
+G_DEFINE_TYPE(HaslGs2Header, hasl_gs2_header, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+hasl_gs2_header_get_property(GObject *obj, guint param_id, GValue *value,
+                             GParamSpec *pspec)
+{
+	HaslGs2Header *header = HASL_GS2_HEADER(obj);
+
+	switch(param_id) {
+	case PROP_STANDARD_MECHANISM:
+		g_value_set_boolean(value,
+		                    hasl_gs2_header_get_standard_mechanism(header));
+		break;
+	case PROP_CHANNEL_BINDING:
+		g_value_set_enum(value, hasl_gs2_header_get_channel_binding(header));
+		break;
+	case PROP_CHANNEL_BINDING_NAME:
+		g_value_set_string(value,
+		                   hasl_gs2_header_get_channel_binding_name(header));
+		break;
+	case PROP_AUTHZID:
+		g_value_set_string(value, hasl_gs2_header_get_authzid(header));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+hasl_gs2_header_set_property(GObject *obj, guint param_id, const GValue *value,
+                             GParamSpec *pspec)
+{
+	HaslGs2Header *header = HASL_GS2_HEADER(obj);
+
+	switch(param_id) {
+	case PROP_STANDARD_MECHANISM:
+		hasl_gs2_header_set_standard_mechanism(header,
+		                                       g_value_get_boolean(value));
+		break;
+	case PROP_CHANNEL_BINDING:
+		hasl_gs2_header_set_channel_binding(header, g_value_get_enum(value));
+		break;
+	case PROP_CHANNEL_BINDING_NAME:
+		hasl_gs2_header_set_channel_binding_name(header,
+		                                         g_value_get_string(value));
+		break;
+	case PROP_AUTHZID:
+		hasl_gs2_header_set_authzid(header, g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+hasl_gs2_header_init(HaslGs2Header *header) {
+	header->standard_mechanism = TRUE;
+	header->channel_binding = HaslGs2HeaderChannelBindingClientNotSupported;
+}
+
+static void
+hasl_gs2_header_finalize(GObject *obj) {
+	HaslGs2Header *header = HASL_GS2_HEADER(obj);
+
+	g_clear_pointer(&header->channel_binding_name, g_free);
+	g_clear_pointer(&header->authzid, g_free);
+
+	G_OBJECT_CLASS(hasl_gs2_header_parent_class)->finalize(obj);
+}
+
+static void
+hasl_gs2_header_class_init(HaslGs2HeaderClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = hasl_gs2_header_finalize;
+	obj_class->get_property = hasl_gs2_header_get_property;
+	obj_class->set_property = hasl_gs2_header_set_property;
+
+	/**
+	 * HaslGs2Header:standard-mechanism:
+	 *
+	 * Whether or not the mechanism is a standard GSS-API mechanism.
+	 *
+	 * Since: 0.3.0
+	 */
+	properties[PROP_STANDARD_MECHANISM] = g_param_spec_boolean(
+		"standard-mechanism", "standard-mechanism",
+		"Whether or not this mechanism is a standard GSS-API mechanism.",
+		TRUE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * HalsGs2Header:channel-binding:
+	 *
+	 * The channel binding flag for this header.
+	 *
+	 * Since: 0.3.0
+	 */
+	properties[PROP_CHANNEL_BINDING] = g_param_spec_enum(
+		"channel-binding", "channel-binding",
+		"The channel binding flag for this header.",
+		HASL_TYPE_GS2_HEADER_CHANNEL_BINDING,
+		HaslGs2HeaderChannelBindingClientNotSupported,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * HaslGs2Header:channel-binding-name:
+	 *
+	 * The name of the channel binding data to use.
+	 *
+	 * If [property@Gs2Header:channel-binding] is set to ClientSupported, this
+	 * must not be %NULL.
+	 *
+	 * Since: 0.3.0
+	 */
+	properties[PROP_CHANNEL_BINDING_NAME] = g_param_spec_string(
+		"channel-binding-name", "channel-binding-name",
+		"The name of the channel binding data to use.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+
+	/**
+	 * HaslGs2Header:authzid:
+	 *
+	 * An optional authzid to use. This is the username to authenticate as when
+	 * using admin/moderator credentials.
+	 *
+	 * Since: 0.3.0
+	 */
+	properties[PROP_AUTHZID] = g_param_spec_string(
+		"authzid", "authzid",
+		"The username to authenticate as.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+HaslGs2Header *
+hasl_gs2_header_new(void) {
+	return g_object_new(HASL_TYPE_GS2_HEADER, NULL);
+}
+
+char *
+hasl_gs2_header_to_string(HaslGs2Header *header, GError **error) {
+	GString *str = NULL;
+
+	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), NULL);
+
+	str = g_string_new("");
+
+	if(!header->standard_mechanism) {
+		g_string_append(str, "F,");
+	}
+
+	/* Add our channel binding information. */
+	if(header->channel_binding == HaslGs2HeaderChannelBindingClientSupported) {
+		gboolean invalid = FALSE;
+
+		if(header->channel_binding_name == NULL ||
+		   header->channel_binding_name[0] == '\0')
+		{
+			g_set_error_literal(error, HASL_GS2_HEADER_DOMAIN, 0,
+			                    "channel binding was set to supported but no "
+			                    "name was given.");
+
+			g_string_free(str, TRUE);
+
+			return NULL;
+		}
+
+		/* Look to see if we have any invalid characters in the name. */
+		invalid = g_regex_match_simple("[^A-Za-z0-9.-]",
+		                               header->channel_binding_name,
+		                               G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT);
+		if(invalid) {
+			g_set_error(error, HASL_GS2_HEADER_DOMAIN, 0,
+			            "channel binding name \"%s\" contains invalid "
+			            "characters. Only alphanumeric . and - are supported",
+			            header->channel_binding_name);
+
+			g_string_free(str, TRUE);
+
+			return NULL;
+		}
+
+		g_string_append_printf(str, "p=%s", header->channel_binding_name);
+	} else if(header->channel_binding == HaslGs2HeaderChannelBindingClientNotSupported)
+	{
+		g_string_append(str, "n");
+	} else if(header->channel_binding == HaslGs2HeaderChannelBindingClientSupportedServerUnknown)
+	{
+		g_string_append(str, "y");
+	} else {
+		g_set_error(error, HASL_GS2_HEADER_DOMAIN, 0,
+		            "unknown channel binding value %d",
+		            header->channel_binding);
+
+		g_string_free(str, TRUE);
+
+		return NULL;
+	}
+	g_string_append(str, ",");
+
+	/* Add the authzid if we have it. */
+	if(header->authzid != NULL && header->authzid[0] != '\0') {
+		char buffer[1024];
+		int res = 0;
+
+		g_strlcpy(buffer, header->authzid, sizeof(buffer));
+		res = stringprep(buffer, sizeof(buffer), 0, stringprep_saslprep);
+		if(res != STRINGPREP_OK) {
+			g_set_error(error, HASL_GS2_HEADER_DOMAIN, 0,
+			            _("stringprep failed: %s"), stringprep_strerror(res));
+
+			g_string_free(str, TRUE);
+
+			return NULL;
+		}
+
+		g_string_append_printf(str, "a=%s", header->authzid);
+	}
+	g_string_append(str, ",");
+
+	/* Free the string but return the non-freed segment. */
+	return g_string_free(str, FALSE);
+}
+
+gboolean
+hasl_gs2_header_get_standard_mechanism(HaslGs2Header *header) {
+	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), TRUE);
+
+	return header->standard_mechanism;
+}
+
+void
+hasl_gs2_header_set_standard_mechanism(HaslGs2Header *header,
+                                       gboolean standard)
+{
+	g_return_if_fail(HASL_IS_GS2_HEADER(header));
+
+	if(header->standard_mechanism != standard) {
+		header->standard_mechanism = standard;
+
+		g_object_notify_by_pspec(G_OBJECT(header),
+		                         properties[PROP_STANDARD_MECHANISM]);
+	}
+}
+
+HaslGs2HeaderChannelBinding
+hasl_gs2_header_get_channel_binding(HaslGs2Header *header) {
+	g_return_val_if_fail(HASL_IS_GS2_HEADER(header),
+	                     HaslGs2HeaderChannelBindingClientNotSupported);
+
+	return header->channel_binding;
+}
+
+void
+hasl_gs2_header_set_channel_binding(HaslGs2Header *header,
+                                    HaslGs2HeaderChannelBinding channel_binding)
+{
+	g_return_if_fail(HASL_IS_GS2_HEADER(header));
+
+	if(header->channel_binding != channel_binding) {
+		header->channel_binding = channel_binding;
+
+		g_object_notify_by_pspec(G_OBJECT(header),
+		                         properties[PROP_CHANNEL_BINDING]);
+	}
+}
+
+const char *
+hasl_gs2_header_get_channel_binding_name(HaslGs2Header *header) {
+	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), NULL);
+
+	return header->channel_binding_name;
+}
+
+void
+hasl_gs2_header_set_channel_binding_name(HaslGs2Header *header,
+                                         const char *name)
+{
+	g_return_if_fail(HASL_IS_GS2_HEADER(header));
+
+	if(g_strcmp0(header->channel_binding_name, name) != 0) {
+		g_free(header->channel_binding_name);
+		header->channel_binding_name = g_strdup(name);
+
+		g_object_notify_by_pspec(G_OBJECT(header),
+		                         properties[PROP_CHANNEL_BINDING_NAME]);
+	}
+}
+
+const char *
+hasl_gs2_header_get_authzid(HaslGs2Header *header) {
+	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), NULL);
+
+	return header->authzid;
+}
+
+void
+hasl_gs2_header_set_authzid(HaslGs2Header *header, const char *authzid) {
+	g_return_if_fail(HASL_IS_GS2_HEADER(header));
+
+	if(g_strcmp0(header->authzid, authzid) != 0) {
+		g_free(header->authzid);
+		header->authzid = g_strdup(authzid);
+
+		g_object_notify_by_pspec(G_OBJECT(header), properties[PROP_AUTHZID]);
+	}
+}
diff --git a/hasl/meson.build b/hasl/meson.build
--- a/hasl/meson.build
+++ b/hasl/meson.build
@@ -1,5 +1,6 @@
 HASL_SOURCES = [
   'haslcontext.c',
+  'haslgs2header.c',
   'haslmechanism.c',
   'haslmechanismanonymous.c',
   'haslmechanismexternal.c',
@@ -10,6 +11,7 @@
 HASL_HEADERS = [
   'haslcontext.h',
   'haslcore.h',
+  'haslgs2header.h',
   'haslmechanism.h',
   'haslmechanismanonymous.h',
   'haslmechanismexternal.h',
@@ -53,6 +55,7 @@
 # Enums
 ###############################################################################
 HASL_ENUM_HEADERS = [
+  'haslgs2header.h',
   'haslmechanism.h',
 ]
 
diff --git a/hasl/tests/meson.build b/hasl/tests/meson.build
--- a/hasl/tests/meson.build
+++ b/hasl/tests/meson.build
@@ -1,6 +1,7 @@
 TESTS = [
   'context',
   'full',
+  'gs2-header',
   'mechanism',
   'mechanism-anonymous',
   'mechanism-external',
diff --git a/hasl/tests/test-gs2-header.c b/hasl/tests/test-gs2-header.c
new file mode 100644
--- /dev/null
+++ b/hasl/tests/test-gs2-header.c
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2023 Hasl Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <hasl.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_hasl_gs2_header_new(void) {
+	HaslGs2Header *header = hasl_gs2_header_new();
+
+	g_assert_true(HASL_IS_GS2_HEADER(header));
+
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_properties(void) {
+	HaslGs2Header *header = NULL;
+	HaslGs2HeaderChannelBinding channel_binding;
+	char *channel_binding_name = NULL;
+	char *authzid = NULL;
+	gboolean standard_mechanism = FALSE;
+
+	header = g_object_new(
+		HASL_TYPE_GS2_HEADER,
+		"standard-mechanism", FALSE,
+		"channel-binding", HaslGs2HeaderChannelBindingClientSupportedServerUnknown,
+		"channel-binding-name", "weee",
+		"authzid", "zerocool",
+		NULL);
+
+	g_assert_true(HASL_IS_GS2_HEADER(header));
+
+	g_object_get(
+		G_OBJECT(header),
+		"standard-mechanism", &standard_mechanism,
+		"channel-binding", &channel_binding,
+		"channel-binding-name", &channel_binding_name,
+		"authzid", &authzid,
+		NULL);
+
+	g_assert_false(standard_mechanism);
+	g_assert_cmpuint(channel_binding, ==,
+	                 HaslGs2HeaderChannelBindingClientSupportedServerUnknown);
+
+	g_assert_cmpstr(channel_binding_name, ==, "weee");
+	g_clear_pointer(&channel_binding_name, g_free);
+
+	g_assert_cmpstr(authzid, ==, "zerocool");
+	g_clear_pointer(&authzid, g_free);
+
+	g_clear_object(&header);
+}
+
+/******************************************************************************
+ * to_string tests
+ *****************************************************************************/
+static void
+test_hasl_gs2_header_to_string_default(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_no_error(error);
+	g_assert_cmpstr(str, ==, "n,,");
+
+	g_clear_pointer(&str, g_free);
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_non_standard_mechanism(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_standard_mechanism(header, FALSE);
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_no_error(error);
+	g_assert_cmpstr(str, ==, "F,n,,");
+
+	g_clear_pointer(&str, g_free);
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_channel_binding_client_supported(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_channel_binding(header,
+	                                    HaslGs2HeaderChannelBindingClientSupported);
+	hasl_gs2_header_set_channel_binding_name(header, "weee");
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_no_error(error);
+	g_assert_cmpstr(str, ==, "p=weee,,");
+
+	g_clear_pointer(&str, g_free);
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_channel_binding_client_supported_name_required(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_channel_binding(header,
+	                                    HaslGs2HeaderChannelBindingClientSupported);
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_error(error, HASL_GS2_HEADER_DOMAIN, 0);
+	g_clear_error(&error);
+	g_assert_null(str);
+
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_channel_binding_client_supported_server_unknown(void)
+{
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_channel_binding(header,
+	                                    HaslGs2HeaderChannelBindingClientSupportedServerUnknown);
+	hasl_gs2_header_set_channel_binding_name(header, "weee");
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_no_error(error);
+	g_assert_cmpstr(str, ==, "y,,");
+
+	g_clear_pointer(&str, g_free);
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_channel_binding_unknown(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_channel_binding(header, 0xFFFF);
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_error(error, HASL_GS2_HEADER_DOMAIN, 0);
+	g_clear_error(&error);
+	g_assert_null(str);
+
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_channel_binding_name_invalid(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+	gboolean matched = FALSE;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_channel_binding(header,
+	                                    HaslGs2HeaderChannelBindingClientSupported);
+	hasl_gs2_header_set_channel_binding_name(header, "🪦");
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_error(error, HASL_GS2_HEADER_DOMAIN, 0);
+
+	matched = g_pattern_match_simple("channel binding name*contains invalid*",
+	                                 error->message);
+	g_assert_true(matched);
+
+	g_clear_error(&error);
+	g_assert_null(str);
+
+	g_clear_object(&header);
+}
+
+static void
+test_hasl_gs2_header_to_string_authzid(void) {
+	HaslGs2Header *header = NULL;
+	GError *error = NULL;
+	char *str = NULL;
+
+	header = hasl_gs2_header_new();
+	hasl_gs2_header_set_authzid(header, "herman");
+
+	str = hasl_gs2_header_to_string(header, &error);
+	g_assert_no_error(error);
+	g_assert_cmpstr(str, ==, "n,a=herman,");
+
+	g_clear_pointer(&str, g_free);
+	g_clear_object(&header);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/hasl/gs2-header/new", test_hasl_gs2_header_new);
+	g_test_add_func("/hasl/gs2-header/properties",
+	                test_hasl_gs2_header_properties);
+
+	g_test_add_func("/hasl/gs2-header/to-string/default",
+	                test_hasl_gs2_header_to_string_default);
+	g_test_add_func("/hasl/gs2-header/to-string/non-standard-mechanism",
+	                test_hasl_gs2_header_to_string_non_standard_mechanism);
+	g_test_add_func("/hasl/gs2-header/to-string/channel-binding-client-supported",
+	                test_hasl_gs2_header_to_string_channel_binding_client_supported);
+	g_test_add_func("/hasl/gs2-header/to-string/channel-binding-client-supported-name-required",
+	                test_hasl_gs2_header_to_string_channel_binding_client_supported_name_required);
+	g_test_add_func("/hasl/gs2-header/to-string/channel-binding-client-supported-server-unknown",
+	                test_hasl_gs2_header_to_string_channel_binding_client_supported_server_unknown);
+	g_test_add_func("/hasl/gs2-header/to-string/channel-binding-unknown",
+	                test_hasl_gs2_header_to_string_channel_binding_unknown);
+	g_test_add_func("/hasl/gs2-header/to-string/channel-binding-name-invalid",
+	                test_hasl_gs2_header_to_string_channel_binding_name_invalid);
+	g_test_add_func("/hasl/gs2-header/to-string/authzid",
+	                test_hasl_gs2_header_to_string_authzid);
+
+	return g_test_run();
+}
