From 7df7cb0b6d9df0081547bff99806411c5300e99d Mon Sep 17 00:00:00 2001
From: Jeroen Janssen <jeroen@laylo.io>
Date: Sun, 27 Aug 2023 11:48:33 +0200
Subject: [PATCH] Revision 47, 2022-12-16 11:26:53 +0100 (Fri, 16 Dec 2022)

---
 Makefile            |   42 +
 filter-dkimverify.8 |   32 +
 ltok.c              | 1876 +++++++++++++++++++++++++++++++++++++++++++
 ltok.h              |  175 ++++
 main.c              | 1759 ++++++++++++++++++++++++++++++++++++++++
 unpack_dns.c        |  295 +++++++
 unpack_dns.h        |   96 +++
 7 files changed, 4275 insertions(+)
 create mode 100644 Makefile
 create mode 100644 filter-dkimverify.8
 create mode 100644 ltok.c
 create mode 100644 ltok.h
 create mode 100644 main.c
 create mode 100644 unpack_dns.c
 create mode 100644 unpack_dns.h

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7bae5ab
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,42 @@
+LOCALBASE?=	/usr/local/
+
+PROG=		filter-dkimverify
+MAN=		filter-dkimverify.8
+BINDIR=		${LOCALBASE}/libexec/smtpd/
+MANDIR=		${LOCALBASE}/man/man
+
+SRCS+=		main.c ltok.c unpack_dns.c
+
+.ifdef HAVE_ED25519
+CFLAGS+=	-DHAVE_ED25519
+.endif
+.ifdef LIBCRYPTOPC
+CRYPT_CFLAGS!=	pkg-config --cflags ${LIBCRYPTOPC}
+CRYPT_LDFLAGS_L!=pkg-config --libs-only-L ${LIBCRYPTOPC}
+CRYPT_LDFLAGS_libdir!=pkg-config --variable libdir ${LIBCRYPTOPC}
+CRYPT_LDFLAGS=	${CRYPT_LDFLAGS_L}
+CRYPT_LDFLAGS+=	-Wl,-rpath,${CRYPT_LDFLAGS_libdir}
+CRYPT_LDADD!=	pkg-config --libs-only-l ${LIBCRYPTOPC}
+.else
+CRYPT_CFLAGS=
+CRYPT_LDFLAGS=
+CRYPT_LDADD=	-lcrypto
+.endif
+
+CFLAGS+=	-I${LOCALBASE}/include -I${.CURDIR}/openbsd-compat 
+CFLAGS+=	-Wall -I${.CURDIR}
+CFLAGS+=	-Wstrict-prototypes -Wmissing-prototypes
+CFLAGS+=	-Wmissing-declarations
+CFLAGS+=	-Wshadow -Wpointer-arith -Wcast-qual
+CFLAGS+=	-Wsign-compare
+CFLAGS+=	${CRYPT_CFLAGS}
+
+LDFLAGS+=	-L${LOCALBASE}/lib
+LDFLAGS+=	${CRYPT_LDFLAGS}
+LDADD+=		${CRYPT_LDADD} -lopensmtpd -levent
+DPADD=		${LIBCRYPTO}
+
+bindir:
+	${INSTALL} -d ${DESTDIR}${BINDIR}
+
+.include <bsd.prog.mk>
diff --git a/filter-dkimverify.8 b/filter-dkimverify.8
new file mode 100644
index 0000000..b49771b
--- /dev/null
+++ b/filter-dkimverify.8
@@ -0,0 +1,32 @@
+.\"	$OpenBSD$
+.\"
+.\" Copyright (c) 2019 Martijn van Duren <martijn@openbsd.org>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate$
+.Dt FILTER-DKIMVERIFY 8
+.Os
+.Sh NAME
+.Nm filter-dkimverify
+.Nd verifies dkim signature of messages
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+verifies the dkim signatures of messages and adds an Authentication-Results
+header to the message.
+.Sh SEE ALSO
+.Xr filter-admdscrub 8
+.Xr filter-dkimsign 8
+.Xr smtpd 8
diff --git a/ltok.c b/ltok.c
new file mode 100644
index 0000000..a80fe73
--- /dev/null
+++ b/ltok.c
@@ -0,0 +1,1876 @@
+/*
+ * Copyright (c) 2020-2022 Martijn van Duren <martijn@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <stddef.h>
+#include <string.h>
+#include <strings.h>
+
+#include "ltok.h"
+
+/* RFC 5234 - Augmented BNF for Syntax Specifications: ABNF */
+const char *
+osmtpd_ltok_skip_alpha(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 0x41 && ptr[0] <= 0x5a) ||
+	    (ptr[0] >= 0x61 && ptr[0] <= 0x7a))
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_bit(const char *ptr, int optional)
+{
+	if (ptr[0] == '0' || ptr[0] == '1')
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_char(const char *ptr, int optional)
+{
+	if (ptr[0] >= 0x01 && ptr[0] <= 0x7f)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_cr(const char *ptr, int optional)
+{
+	if (ptr[0] == 0xd)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_crlf(const char *ptr, int optional)
+{
+	if (ptr[0] == 13 && ptr[1] == 10)
+		return ptr + 2;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_ctl(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 0x00 && ptr[0] <= 0x1f) || ptr[0] == 0x7f)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_digit(const char *ptr, int optional)
+{
+	if (ptr[0] >= 0x30 && ptr[0] <= 0x39)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_dquote(const char *ptr, int optional)
+{
+	if (ptr[0] == 0x22)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_hexdig(const char *ptr, int optional)
+{
+	const char *start = ptr;
+	char l;
+
+	if ((ptr = osmtpd_ltok_skip_digit(ptr, 0)) != NULL)
+		return ptr;
+	l = tolower(start[0]);
+	if (l == 'a' || l == 'b' || l == 'c' || l == 'd' ||
+	    l == 'e' || l == 'f')
+		return start + 1;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_htab(const char *ptr, int optional)
+{
+	if (ptr[0] == 0x9)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_lf(const char *ptr, int optional)
+{
+	if (ptr[0] == 0xa)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_octet(const char *ptr, int optional)
+{
+	return ptr + 1;
+}
+
+const char *
+osmtpd_ltok_skip_sp(const char *ptr, int optional)
+{
+	if (ptr[0] == 0x20)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_vchar(const char *ptr, int optional)
+{
+	if (ptr[0] >= 0x21 && ptr[0] <= 0x7e)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_wsp(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_sp(start, 0)) != NULL ||
+	    (ptr = osmtpd_ltok_skip_htab(start, 0)) != NULL)
+		return ptr;
+	return optional ? start : NULL;
+}
+
+/* RFC 5321 - Simple Mail Transfer Protocol */
+const char *
+osmtpd_ltok_skip_keyword(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_ldh_string(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sub_domain(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_let_dig(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return osmtpd_ltok_skip_ldh_string(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_let_dig(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(start, 0)) == NULL &&
+	    (ptr = osmtpd_ltok_skip_digit(start, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_ldh_string(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+	int letdig = 0;
+
+	while (1) {
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_alpha(prev, 0)) != NULL ||
+		    (ptr = osmtpd_ltok_skip_digit(prev, 0)) != NULL) {
+			letdig = 1;
+			continue;
+		}
+		if (prev[0] == '-') {
+			letdig = 0;
+			ptr = prev + 1;
+			continue;
+		}
+		ptr = prev;
+		break;
+	}
+	if (letdig)
+		return ptr;
+	if (ptr == start)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+/* RFC 5322 - Internet Message Format */
+const char *
+osmtpd_ltok_skip_quoted_pair(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] == '\\' && (
+	    (ptr = osmtpd_ltok_skip_vchar(start + 1, 0)) != NULL ||
+	    (ptr = osmtpd_ltok_skip_wsp(start + 1, 0)) != NULL))
+		return ptr;
+	return osmtpd_ltok_skip_obs_qp(start, optional);
+}
+
+const char *
+osmtpd_ltok_skip_fws(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev = ptr;
+
+	while ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) != NULL)
+		prev = ptr;
+	if ((ptr = osmtpd_ltok_skip_crlf(prev, 1)) == prev)
+		ptr = start;
+	if ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) == NULL)
+		return osmtpd_ltok_skip_obs_fws(start, optional);
+	prev = ptr;
+	while ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) != NULL)
+		prev = ptr;
+	return prev;
+}
+
+const char *
+osmtpd_ltok_skip_ctext(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr[0] >= 33 && ptr[0] <= 39) || (ptr[0] >= 42 && ptr[0] <= 91) ||
+	    (ptr[0] >= 93 && ptr[0] <= 126))
+		return ptr + 1;
+	if ((ptr = osmtpd_ltok_skip_obs_ctext(ptr, 0)) != NULL)
+		return ptr;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_ccontent(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_ctext(ptr, 0)) != NULL)
+		return ptr;
+	if ((ptr = osmtpd_ltok_skip_quoted_pair(start, 0)) != NULL)
+		return ptr;
+	if ((ptr = osmtpd_ltok_skip_comment(start, 0)) != NULL)
+		return ptr;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_comment(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr++[0] != '(')
+		return optional ? start : NULL;
+	while (1) {
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] == ')')
+			return ptr + 1;
+		if ((ptr = osmtpd_ltok_skip_ccontent(ptr, 0)) == NULL)
+			return optional ? start : NULL;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_cfws(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	while (1) {
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_comment(ptr, 0)) == NULL) {
+			ptr = prev;
+			break;
+		}
+	}
+	return ptr == start && !optional ? NULL : ptr;
+}
+
+const char *
+osmtpd_ltok_skip_atext(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(start, 0)) != NULL ||
+	    (ptr = osmtpd_ltok_skip_digit(start, 0)) != NULL)
+		return ptr;
+	ptr = start;
+	if (ptr[0] == '!' || ptr[0] == '#' || ptr[0] == '$' || ptr[0] == '%' ||
+	    ptr[0] == '&' || ptr[0] == '\'' || ptr[0] == '*' || ptr[0] == '+' ||
+	    ptr[0] == '-' || ptr[0] == '/' || ptr[0] == '=' || ptr[0] == '?' ||
+	    ptr[0] == '^' || ptr[0] == '_' || ptr[0] == '`' || ptr[0] == '{' ||
+	    ptr[0] == '|' || ptr[0] == '}' || ptr[0] == '~')
+		return ptr + 1;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_atom(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_atext(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	do {
+		prev = ptr;
+		ptr = osmtpd_ltok_skip_atext(ptr, 1);
+	} while (prev != ptr);
+	return osmtpd_ltok_skip_cfws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_dot_atom_text(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_atext(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	do {
+		prev = ptr;
+		ptr = osmtpd_ltok_skip_atext(ptr, 1);
+	} while (ptr != prev);
+
+	while (ptr[0] == '.') {
+		ptr++;
+		if ((ptr = osmtpd_ltok_skip_atext(ptr, 0)) == NULL)
+			return prev;
+		do {
+			prev = ptr;
+			ptr = osmtpd_ltok_skip_atext(ptr, 1);
+		} while (ptr != prev);
+	}
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_dot_atom(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_dot_atom_text(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return osmtpd_ltok_skip_cfws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_qtext(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] == 33 || (ptr[0] >= 35 && ptr[0] <= 91) ||
+	    (ptr[0] >= 93 && ptr[0] <= 126))
+		return ptr + 1;
+	if ((ptr = osmtpd_ltok_skip_obs_qtext(ptr, 0)) != NULL)
+		return ptr;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_qcontent(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_qtext(ptr, 0)) != NULL)
+		return ptr;
+	return osmtpd_ltok_skip_quoted_pair(start, optional);
+}
+
+const char *
+osmtpd_ltok_skip_quoted_string(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_dquote(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while (1) {
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if ((ptr = osmtpd_ltok_skip_qcontent(ptr, 0)) == NULL)
+			break;
+		prev = ptr;
+	}
+	if ((ptr = osmtpd_ltok_skip_dquote(prev, 0)) == NULL)
+		return optional ? start : NULL;
+	return osmtpd_ltok_skip_cfws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_word(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_atom(ptr, 0)) != NULL)
+		return ptr;
+	return osmtpd_ltok_skip_quoted_string(start, optional);
+}
+
+const char *
+osmtpd_ltok_skip_phrase(const char *ptr, int optional)
+{
+	/* obs-phrase is a superset of phrae */
+	return osmtpd_ltok_skip_obs_phrase(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_name_addr(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	ptr = osmtpd_ltok_skip_display_name(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_angle_addr(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_angle_addr(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if (ptr++[0] != '<')
+		return osmtpd_ltok_skip_obs_angle_addr(start, optional);
+	if ((ptr = osmtpd_ltok_skip_addr_spec(ptr, 0)) == NULL)
+		return osmtpd_ltok_skip_obs_angle_addr(start, optional);
+	if (ptr++[0] != '>')
+		return osmtpd_ltok_skip_obs_angle_addr(start, optional);
+	return osmtpd_ltok_skip_cfws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_display_name(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_phrase(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_addr_spec(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_local_part(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	if (ptr++[0] != '@')
+		return optional ? start : NULL;
+	if ((ptr = osmtpd_ltok_skip_domain(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_local_part(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_dot_atom(ptr, 0)) != NULL)
+		return ptr;
+	ptr = start;
+	if ((ptr = osmtpd_ltok_skip_quoted_string(ptr, 0)) != NULL)
+		return ptr;
+	return osmtpd_ltok_skip_obs_local_part(start, optional);
+}
+
+const char *
+osmtpd_ltok_skip_domain(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_dot_atom(start, 0)) != NULL)
+		return ptr;
+	if ((ptr = osmtpd_ltok_skip_domain_literal(start, 0)) != NULL)
+		return ptr;
+	return osmtpd_ltok_skip_obs_domain(start, optional);
+}
+
+const char *
+osmtpd_ltok_skip_domain_literal(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if (ptr++[0] != '[')
+		return optional ? start : NULL;
+	while (1) {
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_dtext(ptr, 0)) == NULL) {
+			ptr = prev;
+			break;
+		}
+	}
+	if (ptr[0] != ']')
+		return optional ? start : NULL;
+	ptr++;
+	return osmtpd_ltok_skip_cfws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_dtext(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 33 && ptr[0] <= 90) || (ptr[0] >= 94 && ptr[0] <= 126))
+		return ptr + 1;
+	return osmtpd_ltok_skip_obs_dtext(ptr, optional);
+
+}
+
+const char *
+osmtpd_ltok_skip_field_name(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_ftext(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		start = ptr;
+		if ((ptr = osmtpd_ltok_skip_ftext(ptr, 0)) == NULL)
+			return start;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_ftext(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 33 && ptr[0] <= 57) ||
+	    (ptr[0] >= 59 && ptr[0] <= 126))
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_obs_no_ws_ctl(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 1 && ptr[0] <= 8) || ptr[0] == 11 || ptr[0] == 12 ||
+	    (ptr[0] >= 14 && ptr[0] <= 31) || ptr[0] == 127)
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_obs_ctext(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_obs_no_ws_ctl(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_obs_qtext(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_obs_no_ws_ctl(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_obs_qp(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] == '\\' && (
+	    (ptr = osmtpd_ltok_skip_obs_no_ws_ctl(start + 1, 0)) != NULL ||
+	    (ptr = osmtpd_ltok_skip_lf(start + 1, 0)) != NULL ||
+	    (ptr = osmtpd_ltok_skip_cr(start + 1, 0)) != NULL))
+		return ptr;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_obs_phrase(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_word(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_word(ptr, 0)) != NULL)
+			continue;
+		ptr = prev;
+		if (ptr[0] == '.') {
+			ptr++;
+			continue;
+		}
+		if ((ptr = osmtpd_ltok_skip_cfws(ptr, 0)) != NULL)
+			continue;
+		return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_obs_fws(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) != NULL)
+		prev = ptr;
+
+	ptr = prev;
+	while (1) {
+		if ((ptr = osmtpd_ltok_skip_crlf(ptr, 0)) == NULL)
+			return prev;
+		if ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) == NULL)
+			return prev;
+		prev = ptr;
+		while ((ptr = osmtpd_ltok_skip_wsp(ptr, 0)) != NULL)
+			prev = ptr;
+		ptr = prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_obs_angle_addr(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if (ptr++[0] != '<')
+		return optional ? start : NULL;
+	if ((ptr = osmtpd_ltok_skip_obs_route(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	if ((ptr = osmtpd_ltok_skip_addr_spec(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	if (ptr++[0] != '>')
+		return optional ? start : NULL;
+	return osmtpd_ltok_skip_cfws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_obs_route(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_obs_domain_list(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	if (ptr++[0] != ':')
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_obs_domain_list(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev = ptr;
+
+	while (1) {
+		if (ptr[0] == ',') {
+			ptr++;
+			prev = ptr;
+			continue;
+		} else if ((ptr = osmtpd_ltok_skip_cfws(ptr, 0)) != NULL) {
+			prev = ptr;
+			continue;
+		}
+		break;
+	}
+	ptr = prev;
+
+	if (ptr++[0] != '@')
+		return optional ? start : NULL;
+	if ((ptr = osmtpd_ltok_skip_domain(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		if (ptr[0] != ',')
+			break;
+		ptr++;
+		ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+		if (ptr[0] != '@')
+			continue;
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_domain(ptr + 1, 0)) == NULL) {
+			ptr = prev;
+			break;
+		}
+	}
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_obs_local_part(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_word(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while (ptr[0] == '.') {
+		ptr++;
+		if ((ptr = osmtpd_ltok_skip_word(ptr, 0)) == NULL)
+			return prev;
+		prev = ptr;
+	}
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_obs_domain(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_atom(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while (1) {
+		if (ptr++[0] != '.')
+			return prev;
+		if ((ptr = osmtpd_ltok_skip_atom(ptr, 0)) == NULL)
+			return prev;
+		prev = ptr;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_obs_dtext(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_obs_no_ws_ctl(ptr, 0)) != NULL)
+		return ptr;
+	return osmtpd_ltok_skip_quoted_pair(start, optional);
+}
+
+/* RFC 2045 - Multipurpose Internet Mail Extensions */
+const char *
+osmtpd_ltok_skip_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_token(start, 0)) != NULL)
+		return ptr;
+	if ((ptr = osmtpd_ltok_skip_quoted_string(start, 0)) != NULL)
+		return ptr;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_token(const char *ptr, int optional)
+{
+	const char *start;
+	int first = 1;
+
+	while (1) {
+		start = ptr;
+		if ((ptr = osmtpd_ltok_skip_char(start, 0)) != NULL &&
+		    osmtpd_ltok_skip_sp(start, 0) == NULL &&
+		    osmtpd_ltok_skip_ctl(start, 0) == NULL &&
+		    osmtpd_ltok_skip_tspecials(start, 0) == NULL) {
+			first = 0;
+			continue;
+		}
+		return optional || !first ? start : NULL;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_tspecials(const char *ptr, int optional)
+{
+	if (ptr[0] == '(' || ptr[0] == ')' || ptr[0] == '<' || ptr[0] == '>' ||
+	    ptr[0] == '@' || ptr[0] == ',' || ptr[0] == ';' || ptr[0] == ':' ||
+	    ptr[0] == '\\' || ptr[0] == '"' || ptr[0] == '/' || ptr[0] == '[' ||
+	    ptr[0] == ']' || ptr[0] == '?' || ptr[0] == '=')
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_qp_section(const char *ptr, int optional)
+{
+	const char *prev, *last = ptr;
+
+	while (1) {
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_ptext(prev, 0)) != NULL)
+			last = ptr;
+		else if ((ptr = osmtpd_ltok_skip_sp(prev, 0)) == NULL &&
+		    (ptr = osmtpd_ltok_skip_htab(prev, 0)) == NULL)
+			return last;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_ptext(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_hex_octet(start, 0)) == NULL &&
+	    (ptr = osmtpd_ltok_skip_safe_char(start, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_safe_char(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 33 && ptr[0] <= 60) || (ptr[0] >= 62 && ptr[0] <= 126))
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_hex_octet(const char *ptr, int optional)
+{
+	const char *start = ptr;
+	char l;
+
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr++;
+	l = tolower(ptr[0]);
+	if (l == 'a' || l == 'b' || l == 'c' || l == 'd' ||
+	    l == 'e' || l == 'f')
+		ptr++;
+	else if ((ptr = osmtpd_ltok_skip_digit(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	l = tolower(ptr[0]);
+	start = ptr;
+	if (l == 'a' || l == 'b' || l == 'c' || l == 'd' ||
+	    l == 'e' || l == 'f')
+		ptr++;
+	else if ((ptr = osmtpd_ltok_skip_digit(ptr, 0)) == NULL)
+		return start;
+	return ptr;
+}
+
+/* RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures */
+const char *
+osmtpd_ltok_skip_hyphenated_word(const char *ptr, int optional)
+{
+	const char *start = ptr, *end;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+
+	end = ptr;
+	while (1) {
+		start = ptr;
+		if ((ptr = osmtpd_ltok_skip_alpha(start, 0)) != NULL ||
+		    (ptr = osmtpd_ltok_skip_digit(start, 0)) != NULL) {
+			end = ptr;
+		} else if (start[0] == '-')
+			ptr = start + 1;
+		else
+			break;
+	}
+	return end;
+}
+
+const char *
+osmtpd_ltok_skip_alphadigitps(const char *ptr, int optional)
+{
+	const char *end;
+
+	if ((end = osmtpd_ltok_skip_alpha(ptr, 0)) == NULL &&
+	    (end = osmtpd_ltok_skip_digit(ptr, 0)) == NULL &&
+	    ptr[0] != '+' && ptr[0] != '/')
+		return optional ? ptr : NULL;
+	return end == NULL ? ptr + 1 : end;
+}
+
+const char *
+osmtpd_ltok_skip_base64string(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_alphadigitps(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		start = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if ((ptr = osmtpd_ltok_skip_alphadigitps(ptr, 0)) == NULL)
+			break;
+	}
+	ptr = start;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if (ptr[0] == '=') {
+		ptr++;
+		start = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] == '=')
+			ptr++;
+		else
+			ptr = start;
+	} else
+		ptr = start;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_hdr_name(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_field_name(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_qp_hdr_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_dkim_quoted_printable(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_dkim_quoted_printable(const char *ptr, int optional)
+{
+	const char *start;
+
+	while (1) {
+		start = ptr;
+		if ((ptr = osmtpd_ltok_skip_fws(start, 0)) != NULL)
+			continue;
+		if ((ptr = osmtpd_ltok_skip_hex_octet(start, 0)) != NULL)
+			continue;
+		ptr = osmtpd_ltok_skip_dkim_safe_char(start, 0);
+		if (ptr == NULL)
+			break;
+	}
+	return start;
+}
+
+const char *
+osmtpd_ltok_skip_dkim_safe_char(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 0x21 && ptr[0] <= 0x3a) || ptr[0] == 0x3c ||
+	    (ptr[0] >= 0x3e && ptr[0] <= 0x7e))
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_selector(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_sub_domain(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		start = ptr;
+		if (ptr[0] != '.')
+			return start;
+		ptr++;
+		if ((ptr = osmtpd_ltok_skip_sub_domain(ptr, 0)) == NULL)
+			return start;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_tag_list(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_tag_spec(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		/* Starting or trailing ';' */
+		if (ptr[0] != ';')
+			return ptr;
+		ptr++;
+		start = ptr;
+		if ((ptr = osmtpd_ltok_skip_tag_spec(ptr, 0)) == NULL)
+			return start;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_tag_spec(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_tag_name(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr++;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return osmtpd_ltok_skip_fws(ptr, 1);
+}
+
+const char *
+osmtpd_ltok_skip_tag_name(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while ((ptr = osmtpd_ltok_skip_alnumpunc(ptr, 0)) != NULL)
+		prev = ptr;
+	return prev;
+}
+
+const char *
+osmtpd_ltok_skip_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_tval(ptr, 0)) == NULL)
+		return start;
+
+	while (1) {
+		start = ptr;
+		/* FWS contains WSP */
+		if ((ptr = osmtpd_ltok_skip_fws(ptr, 0)) == NULL)
+			return start;
+		prev = ptr;
+		while ((ptr = osmtpd_ltok_skip_fws(ptr, 0)) != NULL)
+			prev = ptr;
+		ptr = prev;
+		if ((ptr = osmtpd_ltok_skip_tval(ptr, 0)) == NULL)
+			return start;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_tval(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_valchar(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while ((ptr = osmtpd_ltok_skip_valchar(ptr, 0)) != NULL)
+		prev = ptr;
+	return prev;
+}
+
+const char *
+osmtpd_ltok_skip_valchar(const char *ptr, int optional)
+{
+	if ((ptr[0] >= 0x21 && ptr[0] <= 0x3A) ||
+	    (ptr[0] >= 0x3C && ptr[0] <= 0x7E))
+		return ptr + 1;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_alnumpunc(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(start, 0)) != NULL)
+		return ptr;
+	if ((ptr = osmtpd_ltok_skip_digit(start, 0)) != NULL)
+		return ptr;
+	if (start[0] == '_')
+		return start + 1;
+	return optional ? start : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_sig_v_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x76)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_v_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_v_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev;
+
+	if ((ptr = osmtpd_ltok_skip_digit(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		prev = ptr;
+		if ((ptr = osmtpd_ltok_skip_digit(ptr, 0)) == NULL)
+			return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_sig_a_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x61)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_a_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_a_tag_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_sig_a_tag_alg(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_a_tag_alg(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_sig_a_tag_k(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	if (ptr[0] != '-')
+		return optional ? start : NULL;
+	ptr++;
+	if ((ptr = osmtpd_ltok_skip_sig_a_tag_h(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_a_tag_k(const char *ptr, int optional)
+{
+	/* sha1 / sha256 covered by x-sig-a-tag-k */
+	return osmtpd_ltok_skip_x_sig_a_tag_k(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_a_tag_h(const char *ptr, int optional)
+{
+	/* rsa / ed25519 covered by x-sig-a-tag-h */
+	return osmtpd_ltok_skip_x_sig_a_tag_h(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_sig_a_tag_k(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev, *end;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while ((end = osmtpd_ltok_skip_alpha(ptr, 0)) != NULL ||
+	    (end = osmtpd_ltok_skip_digit(ptr, 0)) != NULL) {
+		ptr = end;
+		prev = end;
+	}
+	return prev;
+}
+
+const char *
+osmtpd_ltok_skip_x_sig_a_tag_h(const char *ptr, int optional)
+{
+	const char *start = ptr, *prev, *end;
+
+	if ((ptr = osmtpd_ltok_skip_alpha(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	prev = ptr;
+	while ((end = osmtpd_ltok_skip_alpha(ptr, 0)) != NULL ||
+	    (end = osmtpd_ltok_skip_digit(ptr, 0)) != NULL) {
+		ptr = end;
+		prev = end;
+	}
+	return prev;
+}
+
+const char *
+osmtpd_ltok_skip_sig_b_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x62)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_b_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_b_tag_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_sig_b_tag_data(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_b_tag_data(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_base64string(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_bh_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x62 && ptr[0] != 0x68)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 2, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_bh_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_bh_tag_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_sig_bh_tag_data(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_bh_tag_data(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_base64string(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_c_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x63)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_c_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_c_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_sig_c_tag_alg(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	if (ptr[0] == '/') {
+		start = ptr;
+		if ((ptr = osmtpd_ltok_skip_sig_c_tag_alg(ptr, 0)) == NULL)
+			return start;
+	}
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_c_tag_alg(const char *ptr, int optional)
+{
+	/* simple / relaxed covered by x-sig-c-tag-alga */
+	return osmtpd_ltok_skip_x_sig_c_tag_alg(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_sig_c_tag_alg(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_hyphenated_word(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_d_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x64)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_d_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_d_tag_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_domain_name(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_domain_name(const char *ptr, int optional)
+{
+	const char *prev = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_sub_domain(ptr, 0)) == NULL)
+		return optional ? prev : NULL;
+	while (1) {
+		prev = ptr;
+		if (ptr[0] != '.' ||
+		    (ptr = osmtpd_ltok_skip_sub_domain(ptr + 1, 0)) == NULL)
+			return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_sig_h_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x68)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_h_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_h_tag_value(const char *ptr, int optional)
+{
+	const char *prev = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_hdr_name(ptr, 0)) == NULL)
+		return optional ? prev : NULL;
+	while (1) {
+		prev = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] != ':')
+			return prev;
+		ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+		if ((ptr = osmtpd_ltok_skip_hdr_name(ptr, 0)) == NULL)
+			return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_sig_i_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x69)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_i_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_i_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	ptr = osmtpd_ltok_skip_local_part(ptr, 1);
+	if (ptr[0] != '@' ||
+	    (ptr = osmtpd_ltok_skip_domain_name(ptr + 1, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_l_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x6c)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_l_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_l_tag_value(const char *ptr, int optional)
+{
+	size_t i;
+
+	for (i = 0; i < 76; i++) {
+		if (osmtpd_ltok_skip_digit(ptr + i, 0) == NULL)
+			break;
+	}
+	if (i >= 1 && i <= 76)
+		return ptr + i;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_sig_q_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x71)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_q_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_q_tag_value(const char *ptr, int optional)
+{
+	const char *prev = ptr;
+	if ((ptr = osmtpd_ltok_skip_sig_q_tag_method(ptr, 0)) == NULL)
+		return optional ? prev : NULL;
+	while (1) {
+		prev = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] != ':')
+			return prev;
+		ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+		if ((ptr = osmtpd_ltok_skip_sig_q_tag_method(ptr, 0)) == NULL)
+			return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_sig_q_tag_method(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	/* dns/txt covered by x-sig-q-tag-type ["/" x-sig-q-tag-args] */
+	if ((ptr = osmtpd_ltok_skip_x_sig_q_tag_type(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	start = ptr;
+	if (ptr[0] != '/')
+		return ptr;
+	if ((ptr = osmtpd_ltok_skip_x_sig_q_tag_args(ptr, 0)) == NULL)
+		return start;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_x_sig_q_tag_type(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_hyphenated_word(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_sig_q_tag_args(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_qp_hdr_value(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_s_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x73)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_s_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_s_tag_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_selector(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_sig_t_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x74)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_t_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_t_tag_value(const char *ptr, int optional)
+{
+	size_t i;
+
+	for (i = 0; i < 12; i++) {
+		if (osmtpd_ltok_skip_digit(ptr + i, 0) == NULL)
+			break;
+	}
+	if (i >= 1 && i <= 12)
+		return ptr + i;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_sig_x_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x78)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_x_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_x_tag_value(const char *ptr, int optional)
+{
+	size_t i;
+
+	for (i = 0; i < 12; i++) {
+		if (osmtpd_ltok_skip_digit(ptr + i, 0) == NULL)
+			break;
+	}
+	if (i >= 1 && i <= 12)
+		return ptr + i;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_sig_z_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x7a)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_sig_z_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_sig_z_tag_value(const char *ptr, int optional)
+{
+	const char *prev = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_sig_z_tag_copy(ptr, 0)) == NULL)
+		return optional ? ptr : NULL;
+	while (1) {
+		prev = ptr;
+		if (ptr[0] != '|')
+			return prev;
+		ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+		if ((ptr = osmtpd_ltok_skip_sig_z_tag_copy(ptr, 0)) == NULL)
+			return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_sig_z_tag_copy(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_hdr_name(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if (ptr[0] != ':')
+		return optional ? start : NULL;
+	if ((ptr = osmtpd_ltok_skip_qp_hdr_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_v_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x76)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_v_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_v_tag_value(const char *ptr, int optional)
+{
+	if (ptr[0] == 0x44 && ptr[1] == 0x4b && ptr[2] == 0x49 &&
+	    ptr[3] == 0x4d && ptr[4] == 0x31)
+		return ptr + 5;
+	return optional ? ptr : NULL;
+}
+
+const char *
+osmtpd_ltok_skip_key_h_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x68)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_h_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_h_tag_value(const char *ptr, int optional)
+{
+	const char *prev = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_key_h_tag_alg(ptr, 0)) == NULL)
+		return optional ? prev : NULL;
+	while (1) {
+		prev = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] != ':')
+			return prev;
+		ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+		if ((ptr = osmtpd_ltok_skip_key_h_tag_alg(ptr, 0)) == NULL)
+			return prev;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_key_h_tag_alg(const char *ptr, int optional)
+{
+	/* sha1 / sha256 covered by x-key-h-tag-alg */
+	return osmtpd_ltok_skip_x_key_h_tag_alg(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_key_h_tag_alg(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_hyphenated_word(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_key_k_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x6b)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_k_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_k_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_key_k_tag_type(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_k_tag_type(const char *ptr, int optional)
+{
+	/* rsa covered by x-key-k-tag-type */
+	return osmtpd_ltok_skip_x_key_k_tag_type(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_key_k_tag_type(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_hyphenated_word(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_key_n_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x6e)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_n_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_n_tag_value(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_qp_section(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_key_p_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x70)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_p_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_p_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_base64string(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_s_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x73)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_s_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_s_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_key_s_tag_type(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		start = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] != ':')
+			return start;
+		ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+		ptr = osmtpd_ltok_skip_key_s_tag_type(ptr, 0);
+		if (ptr == NULL)
+			return start;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_key_s_tag_type(const char *ptr, int optional)
+{
+	if (ptr[0] == '*')
+		return ptr + 1;
+	/* email covered by x-key-s-tag-type */
+	return osmtpd_ltok_skip_x_key_s_tag_type(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_key_s_tag_type(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_hyphenated_word(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_key_t_tag(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if (ptr[0] != 0x74)
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+	if (ptr[0] != '=')
+		return optional ? start : NULL;
+	ptr = osmtpd_ltok_skip_fws(ptr, 1);
+	if ((ptr = osmtpd_ltok_skip_key_t_tag_value(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
+
+const char *
+osmtpd_ltok_skip_key_t_tag_value(const char *ptr, int optional)
+{
+	const char *start = ptr;
+
+	if ((ptr = osmtpd_ltok_skip_key_t_tag_flag(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	while (1) {
+		start = ptr;
+		ptr = osmtpd_ltok_skip_fws(ptr, 1);
+		if (ptr[0] != ':')
+			return start;
+		ptr = osmtpd_ltok_skip_fws(ptr + 1, 1);
+		ptr = osmtpd_ltok_skip_key_t_tag_flag(ptr, 0);
+		if (ptr == NULL)
+			return start;
+	}
+}
+
+const char *
+osmtpd_ltok_skip_key_t_tag_flag(const char *ptr, int optional)
+{
+	/* y / s covered by x-key-t-tag-flag */
+	return osmtpd_ltok_skip_x_key_t_tag_flag(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_x_key_t_tag_flag(const char *ptr, int optional)
+{
+	return osmtpd_ltok_skip_hyphenated_word(ptr, optional);
+}
+
+const char *
+osmtpd_ltok_skip_ar_pvalue(const char *ptr, int optional)
+{
+	const char *start = ptr, *tmp;
+
+	ptr = osmtpd_ltok_skip_cfws(ptr, 1);
+	if ((tmp = osmtpd_ltok_skip_value(ptr, 0)) != NULL)
+		return tmp;
+	ptr = osmtpd_ltok_skip_local_part(ptr, 1);
+	if (ptr[0] == '@')
+		ptr++;
+	if ((ptr = osmtpd_ltok_skip_domain(ptr, 0)) == NULL)
+		return optional ? start : NULL;
+	return ptr;
+}
diff --git a/ltok.h b/ltok.h
new file mode 100644
index 0000000..cee6800
--- /dev/null
+++ b/ltok.h
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2020-2022 Martijn van Duren <martijn@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* RFC 5234 - Augmented BNF for Syntax Specifications: ABNF */
+const char *osmtpd_ltok_skip_alpha(const char *, int);
+const char *osmtpd_ltok_skip_bit(const char *, int);
+const char *osmtpd_ltok_skip_char(const char *, int);
+const char *osmtpd_ltok_skip_cr(const char *, int);
+const char *osmtpd_ltok_skip_crlf(const char *, int);
+const char *osmtpd_ltok_skip_ctl(const char *, int);
+const char *osmtpd_ltok_skip_digit(const char *, int);
+const char *osmtpd_ltok_skip_dquote(const char *, int);
+const char *osmtpd_ltok_skip_hexdig(const char *, int);
+const char *osmtpd_ltok_skip_htab(const char *, int);
+const char *osmtpd_ltok_skip_lf(const char *, int);
+const char *osmtpd_ltok_skip_octet(const char *, int);
+const char *osmtpd_ltok_skip_sp(const char *, int);
+const char *osmtpd_ltok_skip_vchar(const char *, int);
+const char *osmtpd_ltok_skip_wsp(const char *, int);
+
+/* RFC 5321 - Simple Mail Transfer Protocol */
+const char *osmtpd_ltok_skip_keyword(const char *, int);
+const char *osmtpd_ltok_skip_sub_domain(const char *, int);
+const char *osmtpd_ltok_skip_let_dig(const char *, int);
+const char *osmtpd_ltok_skip_ldh_string(const char *, int);
+
+/* RFC 5322 - Internet Message Format */
+const char *osmtpd_ltok_skip_quoted_pair(const char *, int);
+const char *osmtpd_ltok_skip_fws(const char *, int);
+const char *osmtpd_ltok_skip_ctext(const char *, int);
+const char *osmtpd_ltok_skip_ccontent(const char *, int);
+const char *osmtpd_ltok_skip_comment(const char *, int);
+const char *osmtpd_ltok_skip_cfws(const char *, int);
+const char *osmtpd_ltok_skip_atext(const char *, int);
+const char *osmtpd_ltok_skip_atom(const char *, int);
+const char *osmtpd_ltok_skip_dot_atom_text(const char *, int);
+const char *osmtpd_ltok_skip_dot_atom(const char *, int);
+const char *osmtpd_ltok_skip_qtext(const char *, int);
+const char *osmtpd_ltok_skip_qcontent(const char *, int);
+const char *osmtpd_ltok_skip_quoted_string(const char *, int);
+const char *osmtpd_ltok_skip_word(const char *, int);
+const char *osmtpd_ltok_skip_phrase(const char *, int);
+const char *osmtpd_ltok_skip_name_addr(const char *, int);
+const char *osmtpd_ltok_skip_angle_addr(const char *, int);
+const char *osmtpd_ltok_skip_display_name(const char *, int);
+const char *osmtpd_ltok_skip_addr_spec(const char *, int);
+const char *osmtpd_ltok_skip_local_part(const char *, int);
+const char *osmtpd_ltok_skip_domain(const char *, int);
+const char *osmtpd_ltok_skip_domain_literal(const char *, int);
+const char *osmtpd_ltok_skip_dtext(const char *, int);
+const char *osmtpd_ltok_skip_field_name(const char *, int);
+const char *osmtpd_ltok_skip_ftext(const char *, int);
+const char *osmtpd_ltok_skip_obs_no_ws_ctl(const char *, int);
+const char *osmtpd_ltok_skip_obs_ctext(const char *, int);
+const char *osmtpd_ltok_skip_obs_qtext(const char *, int);
+const char *osmtpd_ltok_skip_obs_qp(const char *, int);
+const char *osmtpd_ltok_skip_obs_phrase(const char *, int);
+const char *osmtpd_ltok_skip_obs_fws(const char *, int);
+const char *osmtpd_ltok_skip_obs_angle_addr(const char *, int);
+const char *osmtpd_ltok_skip_obs_route(const char *, int);
+const char *osmtpd_ltok_skip_obs_domain_list(const char *, int);
+const char *osmtpd_ltok_skip_obs_local_part(const char *, int);
+const char *osmtpd_ltok_skip_obs_domain(const char *, int);
+const char *osmtpd_ltok_skip_obs_dtext(const char *, int);
+
+/* RFC 2045 - Multipurpose Internet Mail Extensions */
+const char *osmtpd_ltok_skip_value(const char *, int);
+const char *osmtpd_ltok_skip_token(const char *, int);
+const char *osmtpd_ltok_skip_tspecials(const char *, int);
+const char *osmtpd_ltok_skip_qp_section(const char *, int);
+const char *osmtpd_ltok_skip_ptext(const char *, int);
+const char *osmtpd_ltok_skip_safe_char(const char *, int);
+const char *osmtpd_ltok_skip_hex_octet(const char *, int);
+
+/* RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures */
+const char *osmtpd_ltok_skip_hyphenated_word(const char *, int);
+const char *osmtpd_ltok_skip_alphadigitps(const char *, int);
+const char *osmtpd_ltok_skip_base64string(const char *, int);
+const char *osmtpd_ltok_skip_hdr_name(const char *, int);
+const char *osmtpd_ltok_skip_qp_hdr_value(const char *, int);
+const char *osmtpd_ltok_skip_dkim_quoted_printable(const char *, int);
+const char *osmtpd_ltok_skip_dkim_safe_char(const char *, int);
+const char *osmtpd_ltok_skip_selector(const char *, int);
+const char *osmtpd_ltok_skip_tag_list(const char *, int);
+const char *osmtpd_ltok_skip_tag_spec(const char *, int);
+const char *osmtpd_ltok_skip_tag_name(const char *, int);
+const char *osmtpd_ltok_skip_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_tval(const char *, int);
+const char *osmtpd_ltok_skip_valchar(const char *, int);
+const char *osmtpd_ltok_skip_alnumpunc(const char *, int);
+const char *osmtpd_ltok_skip_sig_v_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_v_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_a_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_a_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_a_tag_alg(const char *, int);
+const char *osmtpd_ltok_skip_sig_a_tag_k(const char *, int);
+const char *osmtpd_ltok_skip_sig_a_tag_h(const char *, int);
+const char *osmtpd_ltok_skip_x_sig_a_tag_k(const char *, int);
+const char *osmtpd_ltok_skip_x_sig_a_tag_h(const char *, int);
+const char *osmtpd_ltok_skip_sig_b_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_b_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_b_tag_data(const char *, int);
+const char *osmtpd_ltok_skip_sig_bh_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_bh_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_bh_tag_data(const char *, int);
+const char *osmtpd_ltok_skip_sig_c_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_c_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_c_tag_alg(const char *, int);
+const char *osmtpd_ltok_skip_x_sig_c_tag_alg(const char *, int);
+const char *osmtpd_ltok_skip_sig_d_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_d_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_domain_name(const char *, int);
+const char *osmtpd_ltok_skip_sig_h_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_h_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_i_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_i_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_l_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_l_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_q_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_q_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_q_tag_method(const char *, int);
+const char *osmtpd_ltok_skip_x_sig_q_tag_type(const char *, int);
+const char *osmtpd_ltok_skip_x_sig_q_tag_args(const char *, int);
+const char *osmtpd_ltok_skip_sig_s_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_s_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_t_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_t_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_x_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_x_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_z_tag(const char *, int);
+const char *osmtpd_ltok_skip_sig_z_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_sig_z_tag_copy(const char *, int);
+const char *osmtpd_ltok_skip_key_v_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_v_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_h_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_h_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_h_tag_alg(const char *, int);
+const char *osmtpd_ltok_skip_x_key_h_tag_alg(const char *, int);
+const char *osmtpd_ltok_skip_key_k_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_k_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_k_tag_type(const char *, int);
+const char *osmtpd_ltok_skip_x_key_k_tag_type(const char *, int);
+const char *osmtpd_ltok_skip_key_n_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_n_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_p_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_p_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_s_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_s_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_s_tag_type(const char *, int);
+const char *osmtpd_ltok_skip_x_key_s_tag_type(const char *, int);
+const char *osmtpd_ltok_skip_key_t_tag(const char *, int);
+const char *osmtpd_ltok_skip_key_t_tag_value(const char *, int);
+const char *osmtpd_ltok_skip_key_t_tag_flag(const char *, int);
+const char *osmtpd_ltok_skip_x_key_t_tag_flag(const char *, int);
+
+/* Authentication-Results */
+const char *osmtpd_ltok_skip_ar_pvalue(const char *, int);
+
+const char *osmtpd_ltok_domain_uncomment(const char *);
+const char *osmtpd_ltok_from_domain(const char *);
+
+const char *osmtpd_ltok_quoted_string_normalize(const char *);
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..8f324e5
--- /dev/null
+++ b/main.c
@@ -0,0 +1,1759 @@
+/*
+ * Copyright (c) 2022 Martijn van Duren <martijn@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/sha.h>
+#include <openssl/err.h>
+
+#include <arpa/nameser.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <event.h>
+#include <limits.h>
+#include <netdb.h>
+#include <opensmtpd.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <asr.h>
+
+#include "unpack_dns.h"
+#include "ltok.h"
+
+/*
+ * Use RFC8601 (Authentication-Results) codes instead of RFC6376 codes,
+ * since they're more expressive.
+ */
+enum state {
+	DKIM_UNKNOWN,
+	DKIM_PASS,
+	DKIM_FAIL,
+	DKIM_POLICY,
+	DKIM_NEUTRAL,
+	DKIM_TEMPERROR,
+	DKIM_PERMERROR
+};
+
+struct signature {
+	struct header *header;
+	enum state state;
+	const char *state_reason;
+	int v;
+	const char *a;
+	size_t asz;
+	int ak;
+	int sephash;
+	const EVP_MD *ah;
+	char *b;
+	size_t bsz;
+	const char *bheader;
+	/* Make sure padding bits for base64 decoding fit */
+	char bh[EVP_MAX_MD_SIZE + (3 - (EVP_MAX_MD_SIZE % 3))];
+	size_t bhsz;
+	EVP_MD_CTX *bhctx;
+	int c;
+#define CANON_HEADER_SIMPLE	0
+#define CANON_HEADER_RELAXED	1
+#define CANON_HEADER		1
+#define CANON_BODY_SIMPLE	0
+#define CANON_BODY_RELAXED	1 << 1
+#define CANON_BODY		1 << 1
+#define CANON_DONE		1 << 2
+	char d[HOST_NAME_MAX + 1];
+	char **h;
+	const char *i;
+	size_t isz;
+	ssize_t l;
+	int q;
+	char s[HOST_NAME_MAX + 1];
+	time_t t;	/* Signature t=/timestamp */
+#define KT_Y			1
+#define KT_S			1 << 1
+	int kt;		/* Key t=/Flags */
+	time_t x;
+	int z;
+	struct event_asr *query;
+	EVP_PKEY *p;
+};
+
+struct header {
+	struct message *msg;
+	uint8_t readdone;
+	uint8_t parsed;
+	char *buf;
+	size_t buflen;
+	struct signature *sig;
+};
+
+#define AUTHENTICATION_RESULTS_LINELEN 78
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+struct message {
+	struct osmtpd_ctx *ctx;
+	FILE *origf;
+	int parsing_headers;
+	size_t body_whitelines;
+	int has_body;
+	struct header *header;
+	size_t nheaders;
+	int err;
+	int readdone;
+};
+
+void usage(void);
+void dkim_err(struct message *, char *);
+void dkim_errx(struct message *, char *);
+void dkim_conf(const char *, const char *);
+void dkim_dataline(struct osmtpd_ctx *, const char *);
+void dkim_commit(struct osmtpd_ctx *);
+void *dkim_message_new(struct osmtpd_ctx *);
+void dkim_message_free(struct osmtpd_ctx *, void *);
+void dkim_header_add(struct osmtpd_ctx *, const char *);
+void dkim_signature_parse(struct header *);
+void dkim_signature_parse_v(struct signature *, const char *, const char *);
+void dkim_signature_parse_a(struct signature *, const char *, const char *);
+void dkim_signature_parse_b(struct signature *, const char *, const char *);
+void dkim_signature_parse_bh(struct signature *, const char *, const char *);
+void dkim_signature_parse_c(struct signature *, const char *, const char *);
+void dkim_signature_parse_d(struct signature *, const char *, const char *);
+void dkim_signature_parse_h(struct signature *, const char *, const char *);
+void dkim_signature_parse_i(struct signature *, const char *, const char *);
+void dkim_signature_parse_l(struct signature *, const char *, const char *);
+void dkim_signature_parse_q(struct signature *, const char *, const char *);
+void dkim_signature_parse_s(struct signature *, const char *, const char *);
+void dkim_signature_parse_t(struct signature *, const char *, const char *);
+void dkim_signature_parse_x(struct signature *, const char *, const char *);
+void dkim_signature_parse_z(struct signature *, const char *, const char *);
+void dkim_signature_verify(struct signature *);
+void dkim_signature_header(EVP_MD_CTX *, struct signature *, struct header *);
+void dkim_signature_state(struct signature *, enum state, const char *);
+const char *dkim_state2str(enum state);
+void dkim_header_cat(struct osmtpd_ctx *, const char *);
+void dkim_body_parse(struct message *, const char *);
+void dkim_body_verify(struct signature *);
+void dkim_rr_resolve(struct asr_result *, void *);
+void dkim_message_verify(struct message *);
+ssize_t dkim_ar_cat(char **ar, size_t *n, size_t aroff, const char *fmt, ...)
+    __attribute__((__format__ (printf, 4, 5)));
+void dkim_ar_print(struct osmtpd_ctx *, const char *);
+int dkim_key_text_parse(struct signature *, const char *);
+
+char *authservid;
+EVP_ENCODE_CTX *ectx = NULL;
+
+int
+main(int argc, char *argv[])
+{
+	if (argc != 1)
+		osmtpd_errx(1, "Invalid argument count");
+
+	OpenSSL_add_all_digests();
+
+	if (pledge("tmppath stdio dns", NULL) == -1)
+		osmtpd_err(1, "pledge");
+
+	if ((ectx = EVP_ENCODE_CTX_new()) == NULL)
+		osmtpd_err(1, "EVP_ENCODE_CTX_new");
+
+	osmtpd_register_conf(dkim_conf);
+	osmtpd_register_filter_dataline(dkim_dataline);
+	osmtpd_register_filter_commit(dkim_commit);
+	osmtpd_local_message(dkim_message_new, dkim_message_free);
+	osmtpd_run();
+
+	return 0;
+}
+
+void
+dkim_conf(const char *key, const char *value)
+{
+	const char *end;
+
+	if (key == NULL) {
+		if (authservid == NULL)
+			osmtpd_errx(1, "Didn't receive admd config option");
+		return;
+	}
+	if (strcmp(key, "admd") == 0 && authservid == NULL) {
+		if ((authservid = strdup(value)) == NULL)
+			osmtpd_err(1, "malloc");
+		end = osmtpd_ltok_skip_value(authservid, 0);
+		if (authservid + strlen(authservid) != end)
+			osmtpd_errx(1, "Invalid authservid");
+	}
+}
+
+void
+dkim_dataline(struct osmtpd_ctx *ctx, const char *line)
+{
+	struct message *msg = ctx->local_message;
+	size_t i;
+
+	if (msg->err) {
+		if (line[0] == '.' && line[1] =='\0') {
+			msg->readdone = 1;
+			osmtpd_filter_dataline(ctx, ".");
+		}
+		return;
+	}
+
+	if (fprintf(msg->origf, "%s\n", line) < 0) {
+		dkim_err(msg, "Couldn't write to tempfile");
+		return;
+	}
+	if (line[0] == '.') {
+		line++;
+		if (line[0] == '\0') {
+			msg->readdone = 1;
+			for (i = 0; i < msg->nheaders; i++) {
+				if (msg->header[i].sig == NULL)
+					continue;
+				dkim_body_verify(msg->header[i].sig);
+			}
+			dkim_message_verify(msg);
+			return;
+		}
+	}
+	if (msg->parsing_headers) {
+		dkim_header_add(ctx, line);
+		if (line[0] == '\0') {
+			msg->parsing_headers = 0;
+			for (i = 0; i < msg->nheaders; i++) {
+				if (msg->header[i].sig == NULL)
+					continue;
+				if (msg->header[i].sig->query == NULL)
+					dkim_signature_verify(
+					    msg->header[i].sig);
+			}
+		}
+		return;
+	} else {
+		dkim_body_parse(msg, line);
+	}
+}
+
+void
+dkim_commit(struct osmtpd_ctx *ctx)
+{
+	struct message *msg = ctx->local_message;
+
+	if (msg->err)
+		osmtpd_filter_disconnect(ctx, "Internal server error");
+	else
+		osmtpd_filter_proceed(ctx);
+}
+
+void *
+dkim_message_new(struct osmtpd_ctx *ctx)
+{
+	struct message *msg;
+
+	if ((msg = malloc(sizeof(*msg))) == NULL)
+		osmtpd_err(1, NULL);
+
+	if ((msg->origf = tmpfile()) == NULL) {
+		dkim_err(msg, "Can't open tempfile");
+		return NULL;
+	}
+	msg->ctx = ctx;
+	msg->parsing_headers = 1;
+	msg->body_whitelines = 0;
+	msg->has_body = 0;
+	msg->header = NULL;
+	msg->nheaders = 0;
+	msg->err = 0;
+	msg->readdone = 0;
+
+	return msg;
+}
+
+void
+dkim_message_free(struct osmtpd_ctx *ctx, void *data)
+{
+	struct message *msg = data;
+	size_t i, j;
+
+	fclose(msg->origf);
+	for (i = 0; i < msg->nheaders; i++) {
+		if (msg->header[i].sig != NULL) {
+			free(msg->header[i].sig->b);
+			EVP_MD_CTX_free(msg->header[i].sig->bhctx);
+			for (j = 0; msg->header[i].sig->h != NULL &&
+			    msg->header[i].sig->h[j] != NULL; j++)
+				free(msg->header[i].sig->h[j]);
+			free(msg->header[i].sig->h);
+			EVP_PKEY_free(msg->header[i].sig->p);
+		}
+		free(msg->header[i].buf);
+		free(msg->header[i].sig);
+	}
+	free(msg->header);
+	free(msg);
+}
+
+void
+dkim_header_add(struct osmtpd_ctx *ctx, const char *line)
+{
+	struct message *msg = ctx->local_message;
+	const char *start, *end, *verify;
+	struct header *headers;
+	size_t i;
+
+	if (msg->nheaders > 0 &&
+	    msg->header[msg->nheaders - 1].readdone == 0) {
+		if (line[0] != ' ' && line[0] != '\t') {
+			msg->header[msg->nheaders - 1].readdone = 1;
+			start = msg->header[msg->nheaders - 1].buf;
+			end = osmtpd_ltok_skip_field_name(start, 0);
+			/* In case someone uses an obs-optional */
+			if (end != NULL)
+				verify = osmtpd_ltok_skip_wsp(end, 1);
+			if (end != NULL &&
+			    strncasecmp(
+			    start, "DKIM-Signature", end - start) == 0 &&
+			    verify[0] == ':')
+				dkim_signature_parse(
+				    &msg->header[msg->nheaders - 1]);
+			if (line[0] == '\0')
+				return;
+		} else {
+			dkim_header_cat(ctx, line);
+			return;
+		}
+	}
+	if (msg->nheaders % 10 == 0) {
+		if ((headers = recallocarray(msg->header, msg->nheaders,
+		    msg->nheaders + 10, sizeof(*msg->header))) == NULL) {
+			dkim_err(msg, "malloc");
+			return;
+		}
+		msg->header = headers;
+		for (i = 0; i < msg->nheaders; i++) {
+			if (msg->header[i].sig == NULL)
+				continue;
+			msg->header[i].sig->header = &msg->header[i];
+		}
+	}
+	msg->header[msg->nheaders].msg = msg;
+	msg->nheaders++;
+	dkim_header_cat(ctx, line);
+}
+
+void
+dkim_header_cat(struct osmtpd_ctx *ctx, const char *line)
+{
+	struct message *msg = ctx->local_message;
+	struct header *header = &msg->header[msg->nheaders - 1];
+	char *buf;
+
+	size_t needed = header->buflen + strlen(line) + 2;
+
+	if (needed > (header->buflen / 1024) + 1) {
+		buf = reallocarray(header->buf, (needed / 1024) + 1, 1024);
+		if (buf == NULL) {
+			dkim_err(msg, "malloc");
+			return;
+		}
+		header->buf = buf;
+	}
+	header->buflen += snprintf(header->buf + header->buflen,
+	    (((needed / 1024) + 1) * 1024) - header->buflen, "%s%s",
+	    header->buflen == 0 ? "" : "\r\n", line);
+}
+
+void
+dkim_signature_parse(struct header *header)
+{
+	struct signature *sig;
+	struct asr_query *query;
+	const char *buf, *i, *end;
+	char tagname[3];
+	char subdomain[HOST_NAME_MAX + 1];
+	size_t ilen, dlen;
+
+	/* Format checked by dkim_header_add */
+	buf = osmtpd_ltok_skip_field_name(header->buf, 0);
+	buf = osmtpd_ltok_skip_wsp(buf, 1) + 1;
+
+	if ((header->sig = calloc(1, sizeof(*header->sig))) == NULL) {
+		dkim_err(header->msg, "malloc");
+		return;
+	}
+	sig = header->sig;
+	sig->header = header;
+	sig->l = -1;
+	sig->t = -1;
+	sig->x = -1;
+
+	end = osmtpd_ltok_skip_tag_list(buf, 0);
+	if (end == NULL || end[0] != '\0') {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid tag-list");
+		return;
+	}
+
+	while (buf[0] != '\0') {
+		buf = osmtpd_ltok_skip_fws(buf, 1);
+		end = osmtpd_ltok_skip_tag_name(buf, 0);
+
+		/* Unknown tag-name */
+		if ((size_t)(end - buf) >= sizeof(tagname))
+			tagname[0] = '\0';
+		else
+			strlcpy(tagname, buf, (end - buf) + 1);
+		buf = osmtpd_ltok_skip_fws(end, 1);
+		/* '=' */
+		buf = osmtpd_ltok_skip_fws(buf + 1, 1);
+		end = osmtpd_ltok_skip_tag_value(buf, 1);
+		if (strcmp(tagname, "v") == 0)
+			dkim_signature_parse_v(sig, buf, end);
+		else if (strcmp(tagname, "a") == 0)
+			dkim_signature_parse_a(sig, buf, end);
+		else if (strcmp(tagname, "b") == 0)
+			dkim_signature_parse_b(sig, buf, end);
+		else if (strcmp(tagname, "bh") == 0)
+			dkim_signature_parse_bh(sig, buf, end);
+		else if (strcmp(tagname, "c") == 0)
+			dkim_signature_parse_c(sig, buf, end);
+		else if (strcmp(tagname, "d") == 0)
+			dkim_signature_parse_d(sig, buf, end);
+		else if (strcmp(tagname, "h") == 0)
+			dkim_signature_parse_h(sig, buf, end);
+		else if (strcmp(tagname, "i") == 0)
+			dkim_signature_parse_i(sig, buf, end);
+		else if (strcmp(tagname, "l") == 0)
+			dkim_signature_parse_l(sig, buf, end);
+		else if (strcmp(tagname, "q") == 0)
+			dkim_signature_parse_q(sig, buf, end);
+		else if (strcmp(tagname, "s") == 0)
+			dkim_signature_parse_s(sig, buf, end);
+		else if (strcmp(tagname, "t") == 0)
+			dkim_signature_parse_t(sig, buf, end);
+		else if (strcmp(tagname, "x") == 0)
+			dkim_signature_parse_x(sig, buf, end);
+		else if (strcmp(tagname, "z") == 0)
+			dkim_signature_parse_z(sig, buf, end);
+
+		buf = osmtpd_ltok_skip_fws(end, 1);
+		if (buf[0] == ';')
+			buf++;
+		else if (buf[0] != '\0') {
+			dkim_signature_state(sig, DKIM_PERMERROR,
+			    "Invalid tag-list");
+			return;
+		}
+	}
+	if (sig->state != DKIM_UNKNOWN)
+		return;
+
+	if (sig->v != 1)
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing v tag");
+	else if (sig->ah == NULL)
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing a tag");
+	else if (sig->b == NULL)
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing b tag");
+	else if (sig->bhsz == 0)
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing bh tag");
+	else if (sig->d[0] == '\0')
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing d tag");
+	else if (sig->h == NULL)
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing h tag");
+	else if (sig->s[0] == '\0')
+		dkim_signature_state(sig, DKIM_PERMERROR, "Missing s tag");
+	if (sig->state != DKIM_UNKNOWN)
+		return;
+
+	if (sig->i != NULL) {
+		i = osmtpd_ltok_skip_local_part(sig->i, 1) + 1;
+		ilen = sig->isz - (size_t)(i - sig->i);
+		dlen = strlen(sig->d);
+		if (ilen < dlen) {
+			dkim_signature_state(sig, DKIM_PERMERROR,
+			    "i tag not subdomain of d");
+			return;
+		}
+		i += ilen - dlen;
+		if ((i[-1] != '.' && i[-1] != '@') ||
+		    strncasecmp(i, sig->d, dlen) != 0) {
+			dkim_signature_state(sig, DKIM_PERMERROR,
+			    "i tag not subdomain of d");
+			return;
+		}
+	}
+	if (sig->t != -1 && sig->x != -1 && sig->t > sig->x) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "t tag after x tag");
+		return;
+	}
+
+	if ((size_t)snprintf(subdomain, sizeof(subdomain), "%s._domainkey.%s",
+	    sig->s, sig->d) >= sizeof(subdomain)) {
+		dkim_signature_state(sig, DKIM_PERMERROR,
+		    "dns/txt query too long");
+		return;
+	}
+
+	if ((query = res_query_async(subdomain, C_IN, T_TXT, NULL)) == NULL) {
+		dkim_err(header->msg, "res_query_async");
+		return;
+	}
+	if ((sig->query = event_asr_run(query, dkim_rr_resolve, sig)) == NULL) {
+		dkim_err(header->msg, "event_asr_run");
+		asr_abort(query);
+		return;
+	}
+}
+
+void
+dkim_signature_parse_v(struct signature *sig, const char *start, const char *end)
+{
+	if (sig->v != 0) {	/* Duplicate tag */
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate v tag");
+		return;
+	}
+	/* Unsupported version */
+	if (start[0] != '1' || start + 1 != end)
+		dkim_signature_state(sig, DKIM_NEUTRAL, "Unsupported v tag");
+	else
+		sig->v = 1;
+}
+
+void
+dkim_signature_parse_a(struct signature *sig, const char *start, const char *end)
+{
+	char ah[sizeof("sha256")];
+
+	if (sig->ah != NULL) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate a tag");
+		return;
+	}
+
+	if (osmtpd_ltok_skip_sig_a_tag_alg(start, 0) != end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid a tag");
+		return;
+	}
+	sig->a = start;
+	sig->asz = (size_t)(end - start);
+	if (strncmp(start, "rsa-", 4) == 0) {
+		start += 4;
+		sig->ak = EVP_PKEY_RSA;
+		sig->sephash = 0;
+#if HAVE_ED25519
+	} else if (strncmp(start, "ed25519-", 8) == 0) {
+		start += 8;
+		sig->ak = EVP_PKEY_ED25519;
+		sig->sephash = 1;
+#endif
+	} else {
+		dkim_signature_state(sig, DKIM_NEUTRAL, "Unsuppored a tag k");
+		return;
+	}
+	if ((size_t)(end - start) >= sizeof(ah)) {
+		dkim_signature_state(sig, DKIM_NEUTRAL, "Unsuppored a tag h");
+		return;
+	}
+	strlcpy(ah, start, sizeof(ah));
+	ah[end - start] = '\0';
+	if ((sig->ah = EVP_get_digestbyname(ah)) == NULL) {
+		dkim_signature_state(sig, DKIM_NEUTRAL, "Unsuppored a tag h");
+		return;
+	}
+	if ((sig->bhctx = EVP_MD_CTX_new()) == NULL) {
+		dkim_err(sig->header->msg, "EVP_MD_CTX_new");
+		return;
+	}
+	if (EVP_DigestInit_ex(sig->bhctx, sig->ah, NULL) <= 0) {
+		dkim_err(sig->header->msg, "EVP_DigestInit_ex");
+		return;
+	}
+}
+
+void
+dkim_signature_parse_b(struct signature *sig, const char *start, const char *end)
+{
+	int decodesz;
+
+	if (sig->b != NULL) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate b tag");
+		return;
+	}
+	sig->bheader = start;
+	if ((sig->b = malloc((((end - start) / 4) + 1) * 3)) == NULL) {
+		dkim_err(sig->header->msg, "malloc");
+		return;
+	}
+	/* EVP_DecodeBlock doesn't handle internal whitespace */
+	EVP_DecodeInit(ectx);
+	if (EVP_DecodeUpdate(ectx, sig->b, &decodesz, start,
+	    (int)(end - start)) == -1) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid b tag");
+		return;
+	}
+	sig->bsz = decodesz;
+	if (EVP_DecodeFinal(ectx, sig->b + sig->bsz,
+	    &decodesz) == -1) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid b tag");
+		return;
+	}
+	sig->bsz += decodesz;
+}
+
+void
+dkim_signature_parse_bh(struct signature *sig, const char *start, const char *end)
+{
+	const char *b64;
+	size_t n;
+	int decodesz;
+
+	if (sig->bhsz != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate bh tag");
+		return;
+	}
+	/*
+	 * EVP_Decode* expects sig->bh to be large enough,
+	 * so count the actual b64 characters.
+	 */
+	b64 = start;
+	n = 0;
+	while (1) {
+		b64 = osmtpd_ltok_skip_fws(b64, 1);
+		if (osmtpd_ltok_skip_alphadigitps(b64, 0) == NULL)
+			break;
+		n++;
+		b64++;
+	}
+	if (b64[0] == '=') {
+		n++;
+		b64 = osmtpd_ltok_skip_fws(b64 + 1, 1);
+		if (b64[0] == '=') {
+			n++;
+			b64++;
+		}
+	}
+	/* Invalid tag value */
+	if (b64 != end || n % 4 != 0 || (n / 4) * 3 > sizeof(sig->bh)) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid bh tag");
+		return;
+	}
+	/* EVP_DecodeBlock doesn't handle internal whitespace */
+	EVP_DecodeInit(ectx);
+	if (EVP_DecodeUpdate(ectx, sig->bh, &decodesz, start,
+	    (int)(end - start)) == -1) {
+		/* Paranoia check */
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid bh tag");
+		return;
+	}
+	sig->bhsz = decodesz;
+	if (EVP_DecodeFinal(ectx, sig->bh + sig->bhsz, &decodesz) == -1) {
+		/* Paranoia check */
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid bh tag");
+		return;
+	}
+	sig->bhsz += decodesz;
+}
+
+void
+dkim_signature_parse_c(struct signature *sig, const char *start, const char *end)
+{
+	if (sig->c != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate c tag");
+		return;
+	}
+	if (strncmp(start, "simple", 6) == 0) {
+		sig->c = CANON_HEADER_SIMPLE;
+		start += 6;
+	} else if (strncmp(start, "relaxed", 7) == 0) {
+		sig->c = CANON_HEADER_RELAXED;
+		start += 7;
+	} else {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid c tag");
+		return;
+	}
+	if (start[0] == '/') {
+		start++;
+		if (strncmp(start, "simple", 6) == 0) {
+			sig->c |= CANON_BODY_SIMPLE;
+			start += 6;
+		} else if (strncmp(start, "relaxed", 7) == 0) {
+			sig->c |= CANON_BODY_RELAXED;
+			start += 7;
+		} else {
+			dkim_signature_state(sig, DKIM_PERMERROR,
+			    "Invalid c tag");
+			return;
+		}
+	}
+
+	if (start != end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid c tag");
+		return;
+	}
+	sig->c |= CANON_DONE;
+}
+
+void
+dkim_signature_parse_d(struct signature *sig, const char *start, const char *end)
+{
+	if (sig->d[0] != '\0') {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate d tag");
+		return;
+	}
+	if (osmtpd_ltok_skip_sig_d_tag_value(start, 0) != end ||
+	    (size_t)(end - start) >= sizeof(sig->d)) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid d tag");
+		return;
+	}
+	strlcpy(sig->d, start, end - start + 1);
+}
+
+void
+dkim_signature_parse_h(struct signature *sig, const char *start, const char *end)
+{
+	const char *h;
+	size_t n = 0;
+
+	if (sig->h != NULL) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate h tag");
+		return;
+	}
+	if (osmtpd_ltok_skip_sig_h_tag_value(start, 0) < end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid h tag");
+		return;
+	}
+	h = start;
+	while (1) {
+		if ((h = osmtpd_ltok_skip_hdr_name(h, 0)) == NULL) {
+			dkim_signature_state(sig, DKIM_PERMERROR,
+			    "Invalid h tag");
+			return;
+		}
+		n++;
+		/* ';' is part of hdr-name */
+		if (h > end) {
+			h = end;
+			break;
+		}
+		h = osmtpd_ltok_skip_fws(h, 1);
+		if (h[0] != ':')
+			break;
+		h = osmtpd_ltok_skip_fws(h + 1, 1);
+	}
+	if ((sig->h = calloc(n + 1, sizeof(*sig->h))) == NULL) {
+		dkim_err(sig->header->msg, "malloc");
+		return;
+	}
+	n = 0;
+	h = start;
+	while (1) {
+		h = osmtpd_ltok_skip_hdr_name(start, 0);
+		/* ';' is part of hdr-name */
+		if (h > end) {
+			sig->h[n] = strndup(start, end - start);
+			break;
+		}
+		if ((sig->h[n++] = strndup(start, h - start)) == NULL) {
+			dkim_err(sig->header->msg, "malloc");
+			return;
+		}
+		start = osmtpd_ltok_skip_fws(h, 1);
+		if (start[0] != ':')
+			break;
+		start = osmtpd_ltok_skip_fws(start + 1, 1);
+	}
+}
+
+void
+dkim_signature_parse_i(struct signature *sig, const char *start, const char *end)
+{
+	if (sig->i != NULL) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate i tag");
+		return;
+	}
+	if (osmtpd_ltok_skip_sig_i_tag_value(start, 0) != end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid i tag");
+		return;
+	}
+	sig->i = start;
+	sig->isz = (size_t)(end - start);
+}
+
+void
+dkim_signature_parse_l(struct signature *sig, const char *start, const char *end)
+{
+	long long l;
+	char *lend;
+
+	if (sig->l != -1) {	/* Duplicate tag */
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate l tag");
+		return;
+	}
+	errno = 0;
+	l = strtoll(start, &lend, 10);
+	/* > 76 digits in stroll is an overflow */
+	if (osmtpd_ltok_skip_digit(start, 0) == NULL ||
+	    lend != end || errno != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid l tag");
+		return;
+	}
+	if (l > SSIZE_MAX) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "l tag too large");
+		return;
+	}
+	sig->l = (ssize_t)l;
+}
+
+void
+dkim_signature_parse_q(struct signature *sig, const char *start, const char *end)
+{
+	const char *qend;
+
+	if (sig->q != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate q tag");
+		return;
+	}
+
+	while (1) {
+		start = osmtpd_ltok_skip_fws(start, 1);
+		qend = osmtpd_ltok_skip_sig_q_tag_method(start, 0);
+		if (qend == NULL) {
+			dkim_signature_state(sig, DKIM_PERMERROR, "Invalid q tag");
+			return;
+		}
+		if (strncmp(start, "dns/txt", qend - start) == 0)
+			sig->q = 1;
+		start = osmtpd_ltok_skip_fws(qend, 1);
+		if (start[0] != ':')
+			break;
+	}
+	if (start != end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid q tag");
+		return;
+	}
+	if (sig->q != 1) {
+		sig->q = 1;
+		dkim_signature_state(sig, DKIM_NEUTRAL, "No useable q found");
+		return;
+	}
+}
+
+void
+dkim_signature_parse_s(struct signature *sig, const char *start, const char *end)
+{
+	if (sig->s[0] != '\0') {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate s tag");
+		return;
+	}
+	if (osmtpd_ltok_skip_selector(start, 0) != end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid s tag");
+		return;
+	}
+	strlcpy(sig->s, start, end - start + 1);
+}
+
+void
+dkim_signature_parse_t(struct signature *sig, const char *start, const char *end)
+{
+	char *tend;
+
+	if (sig->t != -1) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate t tag");
+		return;
+	}
+	errno = 0;
+	sig->t = strtoll(start, &tend, 10);
+	if (osmtpd_ltok_skip_digit(start, 0) == NULL || tend != end ||
+	    tend - start > 12 || errno != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid t tag");
+		return;
+	}
+}
+
+void
+dkim_signature_parse_x(struct signature *sig, const char *start, const char *end)
+{
+	char *xend;
+
+	if (sig->x != -1) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate x tag");
+		return;
+	}
+	errno = 0;
+	sig->x = strtoll(start, &xend, 10);
+	if (osmtpd_ltok_skip_digit(start, 0) == NULL || xend != end ||
+	    xend - start > 12 || errno != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid x tag");
+		return;
+	}
+}
+
+void
+dkim_signature_parse_z(struct signature *sig, const char *start, const char *end)
+{
+	if (sig->z != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate z tag");
+		return;
+	}
+
+	sig->z = 1;
+	if (osmtpd_ltok_skip_sig_z_tag_value(start, 0) != end) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid z tag");
+		return;
+	}
+}
+
+void
+dkim_signature_verify(struct signature *sig)
+{
+	struct message *msg = sig->header->msg;
+	static EVP_MD_CTX *bctx = NULL;
+	char digest[EVP_MAX_MD_SIZE];
+	unsigned int digestsz;
+	const char *end;
+	size_t i, header;
+
+	if (sig->state != DKIM_UNKNOWN)
+		return;
+
+	if (bctx == NULL) {
+		if ((bctx = EVP_MD_CTX_new()) == NULL) {
+			dkim_errx(msg, "EVP_MD_CTX_new");
+			return;
+		}
+	}
+	EVP_MD_CTX_reset(bctx);
+	if (!sig->sephash) {
+		if (EVP_DigestVerifyInit(bctx, NULL, sig->ah, NULL,
+		    sig->p) != 1) {
+			dkim_errx(msg, "EVP_DigestVerifyInit");
+			return;
+		}
+	} else {
+		if (EVP_DigestInit_ex(bctx, sig->ah, NULL) != 1) {
+			dkim_errx(msg, "EVP_DigestInit_ex");
+			return;
+		}
+	}
+
+	for (i = 0; i < msg->nheaders; i++)
+		msg->header[i].parsed = 0;
+
+	for (header = 0; sig->h[header] != NULL; header++) {
+		for (i = msg->nheaders; i > 0; ) {
+			i--;
+			if (msg->header[i].parsed ||
+			    strncasecmp(msg->header[i].buf, sig->h[header],
+			    strlen(sig->h[header])) != 0 ||
+			    msg->header[i].sig == sig)
+				continue;
+			end = osmtpd_ltok_skip_fws(
+			    msg->header[i].buf + strlen(sig->h[header]), 1);
+			if (end[0] != ':')
+				continue;
+			dkim_signature_header(bctx, sig, &(msg->header[i]));
+			msg->header[i].parsed = 1;
+		}
+	}
+	dkim_signature_header(bctx, sig, sig->header);
+	if (!sig->sephash) {
+		if (EVP_DigestVerifyFinal(bctx, sig->b, sig->bsz) != 1)
+			dkim_signature_state(sig, DKIM_FAIL, "b mismatch");
+	} else {
+		if (EVP_DigestFinal_ex(bctx, digest, &digestsz) == 0) {
+			dkim_errx(msg, "EVP_DigestFinal_ex");
+			return;
+		}
+		if (EVP_DigestVerifyInit(bctx, NULL, NULL, NULL, sig->p) != 1) {
+			dkim_errx(msg, "EVP_DigestVerifyInit");
+			return;
+		}
+		switch (EVP_DigestVerify(bctx, sig->b, sig->bsz, digest,
+		    digestsz)) {
+		case 1:
+			break;
+		case 0:
+			dkim_signature_state(sig, DKIM_FAIL, "b mismatch");
+			break;
+		default:
+			dkim_errx(msg, "EVP_DigestVerify");
+			return;
+		}
+	}
+}
+
+/* EVP_DigestVerifyUpdate is a macro, so we can't alias this on a variable */
+#define dkim_b_digest_update(a, b, c)					\
+	(sig->sephash ? EVP_DigestUpdate((a), (b), (c)) :\
+	    EVP_DigestVerifyUpdate((a), (b), (c)))
+
+void
+dkim_signature_header(EVP_MD_CTX *bctx, struct signature *sig,
+    struct header *header)
+{
+	char c;
+	const char *ptr = header->buf, *end;
+	int inhdrname = 1;
+	int canon = sig->c & CANON_HEADER;
+
+	for (ptr = header->buf; ptr[0] != '\0'; ptr++) {
+		if (inhdrname) {
+			if (canon == CANON_HEADER_RELAXED) {
+				ptr = osmtpd_ltok_skip_fws(ptr, 1);
+				c = tolower(ptr[0]);
+			} else
+				c = ptr[0];
+			if (c == ':') {
+				inhdrname = 0;
+				if (canon == CANON_HEADER_RELAXED)
+					ptr = osmtpd_ltok_skip_fws(
+					    ptr + 1, 1) - 1;
+			}
+			if (dkim_b_digest_update(bctx, &c, 1) == 0) {
+				dkim_errx(sig->header->msg,
+				    "dkim_b_digest_update");
+				return;
+			}
+			continue;
+		}
+		end = osmtpd_ltok_skip_fws(ptr, 1);
+		if (end == ptr) {
+			if (sig->header == header && ptr == sig->bheader) {
+				ptr = osmtpd_ltok_skip_tag_value(
+				    ptr, 0) - 1;
+				continue;
+			}
+			if (dkim_b_digest_update(bctx, ptr, 1) == 0) {
+				dkim_errx(sig->header->msg,
+				    "dkim_b_digest_update");
+				return;
+			}
+		} else {
+			if (canon == CANON_HEADER_RELAXED) {
+				if (end[0] == '\0')
+					continue;
+				if (dkim_b_digest_update(bctx, " ", 1) == 0) {
+					dkim_errx(sig->header->msg,
+					    "dkim_b_digest_update");
+					return;
+				}
+			} else {
+				if (dkim_b_digest_update(bctx, ptr,
+				    end - ptr) == 0) {
+					dkim_errx(sig->header->msg,
+					    "dkim_b_digest_update");
+					return;
+				}
+			}
+			ptr = end - 1;
+		}
+			
+	}
+	if (sig->header != header) {
+		if (dkim_b_digest_update(bctx, "\r\n", 2) == 0) {
+			dkim_errx(sig->header->msg, "dkim_b_digest_update");
+			return;
+		}
+	}
+}
+
+void
+dkim_signature_state(struct signature *sig, enum state state,
+    const char *reason)
+{
+	if (sig->query != NULL) {
+		event_asr_abort(sig->query);
+		sig->query = NULL;
+	}
+	switch (sig->state) {
+	case DKIM_UNKNOWN:
+		break;
+	case DKIM_PASS:
+	case DKIM_FAIL:
+		osmtpd_errx(1, "Unexpected transition");
+	case DKIM_POLICY:
+		if (state == DKIM_PASS)
+			return;
+		break;
+	case DKIM_NEUTRAL:
+		if (state == DKIM_PASS)
+			return;
+		if (state == DKIM_TEMPERROR || state == DKIM_PERMERROR)
+			break;
+		osmtpd_errx(1, "Unexpected transition");
+	case DKIM_TEMPERROR:
+		if (state == DKIM_PERMERROR)
+			break;
+		return;
+	case DKIM_PERMERROR:
+		return;
+	}
+	sig->state = state;
+	sig->state_reason = reason;
+}
+
+const char *
+dkim_state2str(enum state state)
+{
+	switch (state)
+	{
+	case DKIM_UNKNOWN:
+		return "unknown";
+	case DKIM_PASS:
+		return "pass";
+	case DKIM_FAIL:
+		return "fail";
+	case DKIM_POLICY:
+		return "policy";
+	case DKIM_NEUTRAL:
+		return "neutral";
+	case DKIM_TEMPERROR:
+		return "temperror";
+	case DKIM_PERMERROR:
+		return "permerror";
+	}
+}
+
+void
+dkim_rr_resolve(struct asr_result *ar, void *arg)
+{
+	struct signature *sig = arg;
+	char key[UINT16_MAX + 1];
+	const char *rr_txt;
+	size_t keylen, cstrlen;
+	struct unpack pack;
+	struct dns_header h;
+	struct dns_query q;
+	struct dns_rr rr;
+
+	sig->query = NULL;
+
+	if (ar->ar_h_errno == TRY_AGAIN || ar->ar_h_errno == NO_RECOVERY) {
+		dkim_signature_state(sig, DKIM_TEMPERROR,
+		    hstrerror(ar->ar_h_errno));
+		goto verify;
+	}
+	if (ar->ar_h_errno != NETDB_SUCCESS) {
+		dkim_signature_state(sig, DKIM_PERMERROR,
+		    hstrerror(ar->ar_h_errno));
+		goto verify;
+	}
+
+	unpack_init(&pack, ar->ar_data, ar->ar_datalen);
+	if (unpack_header(&pack, &h) != 0 ||
+	    unpack_query(&pack, &q) != 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Invalid dns/txt");
+		goto verify;
+	}
+	for (; h.ancount > 0; h.ancount--) {
+		unpack_rr(&pack, &rr);
+		if (rr.rr_type != T_TXT)
+			continue;
+
+		keylen = 0;
+		rr_txt = rr.rr.other.rdata;
+		while (rr.rr.other.rdlen > 0) {
+			cstrlen = ((const unsigned char *)rr_txt)[0];
+			if (cstrlen >= rr.rr.other.rdlen ||
+			    keylen + cstrlen >= sizeof(key))
+				break;
+			/*
+			 * RFC 6376 Section 3.6.2.2
+			 * Strings in a TXT RR MUST be concatenated together
+			 * before use with no intervening whitespace.
+			 */
+			strlcpy(key + keylen, rr_txt + 1, cstrlen + 1);
+			rr.rr.other.rdlen -= (cstrlen + 1);
+			rr_txt += (cstrlen + 1);
+			keylen += cstrlen;
+		}
+		if (rr.rr.other.rdlen > 0)	/* Invalid TXT RDATA */
+			continue;
+
+		if (dkim_key_text_parse(sig, key))
+			break;
+	}
+
+	if (h.ancount == 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR,
+		    "No matching key found");
+	} else {
+		/* Only verify if all headers have been read */
+		if (!sig->header->msg->parsing_headers)
+			dkim_signature_verify(sig);
+	}
+ verify:
+	free(ar->ar_data);
+	dkim_message_verify(sig->header->msg);
+}
+
+int
+dkim_key_text_parse(struct signature *sig, const char *key)
+{
+	char tagname, *hashname;
+	const char *end, *tagvend;
+	char pkraw[UINT16_MAX] = "", pkimp[UINT16_MAX];
+	size_t pkrawlen = 0, pkoff, linelen;
+	int h = 0, k = 0, n = 0, p = 0, s = 0, t = 0, first = 1;
+	BIO *bio;
+#ifdef HAVE_ED25519
+	size_t pklen;
+	int tmp;
+#endif
+
+	key = osmtpd_ltok_skip_fws(key, 1);
+	/* Validate syntax early */
+	if ((end = osmtpd_ltok_skip_tag_list(key, 0)) == NULL)
+		return 0;
+
+	while (key[0] != '\0') {
+		key = osmtpd_ltok_skip_fws(key, 1);
+		if ((end = osmtpd_ltok_skip_tag_name(key, 0)) == NULL)
+			return 0;
+
+		if ((size_t)(end - key) != 1)
+			tagname = '\0';
+		else
+			tagname = key[0];
+		key = osmtpd_ltok_skip_fws(end, 1);
+		/* '=' */
+		if (key[0] != '=')
+			return 0;
+		key = osmtpd_ltok_skip_fws(key + 1, 1);
+		if ((end = osmtpd_ltok_skip_tag_value(key, 0)) == NULL)
+			return 0;
+		switch (tagname) {
+		case 'v':
+			/*
+			 * RFC 6376 section 3.6.1, v=:
+			 * RECOMMENDED...This tag MUST be the first tag in the
+			 * record.
+			 */
+			if (!first ||
+			    osmtpd_ltok_skip_key_v_tag_value(key, 0) != end)
+				return 0;
+			key = end;
+			break;
+		case 'h':
+			if (h != 0)	/* Duplicate tag */
+				return 0;
+			/* Invalid tag value */
+			if (osmtpd_ltok_skip_key_h_tag_value(key, 0) != end)
+				return 0;
+			while (1) {
+				if ((tagvend = osmtpd_ltok_skip_key_h_tag_alg(
+				    key, 0)) == NULL)
+					break;
+				hashname = strndup(key, tagvend - key);
+				if (hashname == NULL) {
+					dkim_err(sig->header->msg, "malloc");
+					return 0;
+				}
+				if (EVP_get_digestbyname(hashname) == sig->ah) {
+					free(hashname);
+					h = 1;
+					break;
+				}
+				free(hashname);
+				key = osmtpd_ltok_skip_fws(tagvend, 1);
+				if (key[0] != ':')
+					break;
+				key = osmtpd_ltok_skip_fws(key + 1, 1);
+			}
+			if (h != 1)
+				return 0;
+			key = end;
+			break;
+		case 'k':
+			if (k != 0)	/* Duplicate tag */
+				return 0;
+			k = 1;
+			if (strncmp(key, "rsa", end - key) == 0) {
+				if (sig->ak != EVP_PKEY_RSA)
+					return 0;
+#if HAVE_ED25519
+			} else if (strncmp(key, "ed25519", end - key) == 0) {
+				if (sig->ak != EVP_PKEY_ED25519)
+					return 0;
+#endif
+			} else
+				return 0;
+			key = end;
+			break;
+		case 'n':
+			if (n != 0)	/* Duplicate tag */
+				return 0;
+			n = 1;
+			/* semicolon is part of safe-char */
+			if (osmtpd_ltok_skip_key_n_tag_value(key, 0) < end)
+				return 0;
+			key = end;
+			break;
+		case 'p':
+			if (p != 0)	/* Duplicate tag */
+				return 0;
+			p = 1;
+			while (1) {
+				key = osmtpd_ltok_skip_fws(key, 1);
+				if (osmtpd_ltok_skip_alphadigitps(
+				    key, 0) == NULL)
+					break;
+				pkraw[pkrawlen++] = key++[0];
+				if (pkrawlen >= sizeof(pkraw))
+					return 0;
+			}
+			if (key[0] == '=') {
+				pkraw[pkrawlen++] = '=';
+				key = osmtpd_ltok_skip_fws(key + 1, 1);
+				if (pkrawlen >= sizeof(pkraw))
+					return 0;
+				if (key[0] == '=') {
+					pkraw[pkrawlen++] = '=';
+					key++;
+					if (pkrawlen >= sizeof(pkraw))
+						return 0;
+				}
+			}
+			/* Invalid tag value */
+			if (pkrawlen % 4 != 0 || key != end)
+				return 0;
+			break;
+		case 's':
+			if (s != 0)	/* Duplicate tag */
+				return 0;
+			/* Invalid tag value */
+			if (osmtpd_ltok_skip_key_s_tag_value(key, 0) != end)
+				return 0;
+			while (1) {
+				if ((tagvend =
+				    osmtpd_ltok_skip_key_s_tag_type(
+				    key, 0)) == NULL)
+					break;
+				if (strncmp(key, "*", tagvend - key) == 0 ||
+				    strncmp(key, "email", tagvend - key) == 0) {
+					s = 1;
+					break;
+				}
+				key = osmtpd_ltok_skip_fws(tagvend, 1);
+				if (key[0] != ':')
+					break;
+				key = osmtpd_ltok_skip_fws(key + 1, 1);
+			}
+			if (s != 1)
+				return 0;
+			key = end;
+			break;
+		case 't':
+			if (t != 0)	/* Duplicate tag */
+				return 0;
+			t = 1;
+			if (osmtpd_ltok_skip_key_t_tag_value(key, 0) != end)
+				return 0;
+			while (1) {
+				tagvend = osmtpd_ltok_skip_key_t_tag_flag(
+				    key, 0);
+				if (strncmp(key, "y", tagvend - key) == 0)
+					sig->kt |= KT_Y;
+				else if (strncmp(key, "s", tagvend - key) == 0)
+					sig->kt |= KT_S;
+				key = osmtpd_ltok_skip_fws(tagvend, 1);
+				if (key[0] != ':')
+					break;
+				key = osmtpd_ltok_skip_fws(key + 1, 1);
+			}
+			break;
+		default:
+			key = end;
+			break;
+		}
+
+		first = 0;
+		key = osmtpd_ltok_skip_fws(key, 1);
+		if (key[0] == ';')
+			key++;
+		else if (key[0] != '\0')
+			return 0;
+	}
+
+	if (!p)					/* Missing tag */
+		return 0;
+	if (k == 0 && sig->ak != EVP_PKEY_RSA)	/* Default to RSA */
+		return 0;
+
+	if (pkraw[0] == '\0') {
+		dkim_signature_state(sig, DKIM_PERMERROR, "Key is revoked");
+		return 1;
+	}
+
+	switch (sig->ak) {
+	case EVP_PKEY_RSA:
+		pkoff = strlcpy(pkimp, "-----BEGIN PUBLIC KEY-----\n",
+		    sizeof(pkimp));
+		linelen = 0;
+		for (key = pkraw; key[0] != '\0';) {
+			if (pkoff + 2 >= sizeof(pkimp))
+				return 0;
+			pkimp[pkoff++] = key++[0];
+			if (++linelen == 64) {
+				pkimp[pkoff++] = '\n';
+				linelen = 0;
+			}
+		}
+		/* Leverage pkoff check in loop */
+		if (linelen != 0)
+			pkimp[pkoff++] = '\n';
+		/* PEM_read_bio_PUBKEY will catch truncated keys */
+		pkoff += strlcpy(pkimp + pkoff, "-----END PUBLIC KEY-----\n",
+		    sizeof(pkimp) - pkoff);
+		if ((bio = BIO_new_mem_buf(pkimp, pkoff)) == NULL) {
+			dkim_err(sig->header->msg, "BIO_new_mem_buf");
+			return 1;
+		}
+		sig->p = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
+		BIO_free(bio);
+		break;
+#if HAVE_ED25519
+	case EVP_PKEY_ED25519:
+		if ((pkrawlen / 4) * 3 >= sizeof(pkimp))
+			return 0;
+		EVP_DecodeInit(ectx);
+		if (EVP_DecodeUpdate(ectx, pkimp, &tmp, pkraw, pkrawlen) == -1)
+			return 0;
+		pklen = tmp;
+		if (EVP_DecodeFinal(ectx, pkimp, &tmp) == -1)
+			return 0;
+		pklen += tmp;
+		sig->p = EVP_PKEY_new_raw_public_key(sig->ak, NULL, pkimp,
+		    pklen);
+		break;
+#endif
+	}
+	if (sig->p == NULL) {
+		/*
+		 * XXX No clue how to differentiate between invalid key and
+		 * temporary failure like *alloc.
+		 * Assume invalid key, because it's more likely.
+		 */
+		return 0;
+	}
+	return 1;
+}
+
+void
+dkim_body_parse(struct message *msg, const char *line)
+{
+	struct signature *sig;
+	const char *end = line, *hash, *prev;
+	size_t hashn, len, i;
+	int wsp, ret;
+
+	if (line[0] == '\0') {
+		msg->body_whitelines++;
+		return;
+	}
+
+	while (msg->body_whitelines-- > 0) {
+		for (i = 0; i < msg->nheaders; i++) {
+			if ((sig = msg->header[i].sig) == NULL ||
+			    sig->state != DKIM_UNKNOWN)
+				continue;
+			hashn = sig->l == -1 ? 2 : MIN(2, sig->l);
+			sig->l -= sig->l == -1 ? 0 : hashn;
+			if (EVP_DigestUpdate(sig->bhctx, "\r\n", hashn) == 0) {
+				dkim_errx(msg, "EVP_DigestUpdate");
+				return;
+			}
+		}
+	}
+	msg->body_whitelines = 0;
+	msg->has_body = 1;
+
+	while (line[0] != '\0') {
+		while (1) {
+			prev = end;
+			if ((end = osmtpd_ltok_skip_wsp(end, 0)) == NULL)
+				break;
+		}
+		end = prev;
+		wsp = end != line;
+		if (!wsp) {
+			while (osmtpd_ltok_skip_wsp(end, 0) == NULL &&
+			    end[0] != '\0')
+				end++;
+		}
+		for (i = 0; i < msg->nheaders; i++) {
+			sig = msg->header[i].sig;
+			if (sig == NULL || sig->state != DKIM_UNKNOWN)
+				continue;
+			if (wsp &&
+			    (sig->c & CANON_BODY) == CANON_BODY_RELAXED) {
+				hash = " ";
+				len = end[0] == '\0' ? 0 : 1;
+			} else {
+				hash = line;
+				len = (size_t)(end - line);
+			}
+			hashn = sig->l == -1 ? len : MIN(len, (size_t)sig->l);
+			sig->l -= sig->l == -1 ? 0 : hashn;
+			ret = EVP_DigestUpdate(sig->bhctx, hash, hashn);
+			if (ret == 0) {
+				dkim_errx(msg, "EVP_DigestUpdate");
+				return;
+			}
+		}
+		line = end;
+	}
+	for (i = 0; i < msg->nheaders; i++) {
+		sig = msg->header[i].sig;
+		if (sig == NULL || sig->state != DKIM_UNKNOWN)
+			continue;
+		hashn = sig->l == -1 ? 2 : MIN(2, sig->l);
+		sig->l -= sig->l == -1 ? 0 : hashn;
+		ret = EVP_DigestUpdate(sig->bhctx, "\r\n", hashn);
+		if (ret == 0) {
+			dkim_errx(msg, "EVP_DigestUpdate");
+			return;
+		}
+	}
+}
+
+void
+dkim_body_verify(struct signature *sig)
+{
+	unsigned char digest[EVP_MAX_MD_SIZE];
+	unsigned int digestsz;
+
+	if (sig->state != DKIM_UNKNOWN)
+		return;
+
+	if ((sig->c & CANON_BODY) == CANON_BODY_SIMPLE &&
+	    !sig->header->msg->has_body) {
+		if (EVP_DigestUpdate(sig->bhctx, "\r\n",
+		    sig->l == -1 ? 2 : MIN(2, sig->l)) <= 0) {
+			dkim_errx(sig->header->msg,
+			    "Can't update hash context");
+			return;
+		}
+	}
+	if (sig->l > 0) {
+		dkim_signature_state(sig, DKIM_PERMERROR,
+		    "l tag larger than body");
+		return;
+	}
+
+	if (EVP_DigestFinal_ex(sig->bhctx, digest, &digestsz) == 0) {
+		dkim_errx(sig->header->msg, "EVP_DigestFinal_ex");
+		return;
+	}
+
+	if (digestsz != sig->bhsz || memcmp(digest, sig->bh, digestsz) != 0)
+		dkim_signature_state(sig, DKIM_FAIL, "bh mismatch");
+}
+
+void
+dkim_message_verify(struct message *msg)
+{
+	struct signature *sig;
+	size_t i;
+	ssize_t n, aroff = 0;
+	int found = 0;
+	char *line = NULL;
+	size_t linelen = 0;
+
+	if (!msg->readdone)
+		return;
+
+	for (i = 0; i < msg->nheaders; i++) {
+		if (msg->header[i].sig == NULL)
+			continue;
+		if (msg->header[i].sig->query != NULL)
+			return;
+		if (msg->header[i].sig->state != DKIM_UNKNOWN)
+			continue;
+		dkim_signature_state(msg->header[i].sig, DKIM_PASS, NULL);
+	}
+	
+	if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
+	    "Authentication-Results: %s", authservid)) == -1) {
+		dkim_err(msg, "malloc");
+		goto fail;
+	}
+	for (i = 0; i < msg->nheaders; i++) {
+		sig = msg->header[i].sig;
+		if (sig == NULL)
+			continue;
+		found = 1;
+		if ((aroff = dkim_ar_cat(&line, &linelen, aroff, "; dkim=%s",
+		    dkim_state2str(sig->state))) == -1) {
+			dkim_err(msg, "malloc");
+			goto fail;
+		}
+		if (sig->state_reason != NULL) {
+			if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
+			    " reason=\"%s\"", sig->state_reason)) == -1) {
+				dkim_err(msg, "malloc");
+				goto fail;
+			}
+		}
+		if (sig->s[0] != '\0') {
+			if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
+			    " header.s=%s", sig->s)) == -1) {
+				dkim_err(msg, "malloc");
+				goto fail;
+			}
+		}
+		if (sig->d[0] != '\0') {
+			if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
+			    " header.d=%s", sig->d)) == -1) {
+				dkim_err(msg, "malloc");
+				goto fail;
+			}
+		}
+		/*
+		 * Don't print i-tag, since localpart can be a quoted-string,
+		 * which can contain FWS and CFWS.
+		 */
+		if (sig->a != NULL) {
+			if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
+			    " header.a=%.*s", (int)sig->asz, sig->a)) == -1) {
+				dkim_err(msg, "malloc");
+				goto fail;
+			}
+		}
+	}
+	if (!found) {
+		aroff = dkim_ar_cat(&line, &linelen, aroff, "; dkim=none");
+		if (aroff == -1) {
+			dkim_err(msg, "malloc");
+			goto fail;
+		}
+	}
+	dkim_ar_print(msg->ctx, line);
+
+	rewind(msg->origf);
+	while ((n = getline(&line, &linelen, msg->origf)) != -1) {
+		line[n - 1] = '\0';
+		osmtpd_filter_dataline(msg->ctx, "%s", line);
+	}
+	if (ferror(msg->origf))
+		dkim_err(msg, "getline");
+ fail:
+	free(line);
+	return;
+}
+
+void
+dkim_ar_print(struct osmtpd_ctx *ctx, const char *start)
+{
+	const char *scan, *checkpoint, *ncheckpoint;
+	size_t arlen = 0;
+	int first = 1, arid = 1;
+
+	checkpoint = start;
+	ncheckpoint = osmtpd_ltok_skip_hdr_name(start, 0) + 1;
+	for (scan = start; scan[0] != '\0'; scan++) {
+		if (scan[0] == '\t')
+			arlen = (arlen + 8) & ~7;
+		else
+			arlen++;
+		if (arlen >= AUTHENTICATION_RESULTS_LINELEN) {
+			osmtpd_filter_dataline(ctx, "%s%.*s", first ? "" : "\t",
+			    (int)((checkpoint == start ?
+			    ncheckpoint : checkpoint) - start), start);
+			start = osmtpd_ltok_skip_cfws(checkpoint, 1);
+			scan = start;
+			arlen = 8;
+			first = 0;
+		}
+		if (scan == ncheckpoint) {
+			checkpoint = ncheckpoint;
+			ncheckpoint = osmtpd_ltok_skip_cfws(ncheckpoint, 1);
+			/* authserv-id */
+			if (arid) {
+				ncheckpoint = osmtpd_ltok_skip_value(
+				    ncheckpoint, 0);
+				arid = 0;
+			/* methodspec */
+			} else if (strncmp(ncheckpoint, "dkim",
+			    sizeof("dkim") - 1) == 0) {
+				ncheckpoint = osmtpd_ltok_skip_keyword(
+				    ncheckpoint + sizeof("dkim"), 0);
+			/* reasonspec */
+			} else if (strncmp(ncheckpoint, "reason",
+			    sizeof("reason") - 1) == 0) {
+				ncheckpoint = osmtpd_ltok_skip_value(
+				    ncheckpoint + sizeof("reason"), 0);
+			/* propspec */
+			} else {
+				ncheckpoint += sizeof("header.x=") - 1;
+				ncheckpoint = osmtpd_ltok_skip_ar_pvalue(
+				    ncheckpoint, 0);
+				if (ncheckpoint[0] == ';')
+					ncheckpoint++;
+			}
+		}
+	}
+	osmtpd_filter_dataline(ctx, "%s%s", first ? "" : "\t", start);
+}
+
+ssize_t
+dkim_ar_cat(char **ar, size_t *n, size_t aroff, const char *fmt, ...)
+{
+	va_list ap;
+	char *artmp;
+	int size;
+	size_t nn;
+
+	va_start(ap, fmt);
+	size = vsnprintf(*ar + aroff, *n - aroff, fmt, ap);
+	va_end(ap);
+	if (size + aroff <= *n)
+		return (ssize_t)size + aroff;
+	nn = (((aroff + size)  / 256) + 1) * 256;
+	artmp = realloc(*ar, nn);
+	if (artmp == NULL)
+		return -1;
+	*ar = artmp;
+	*n = nn;
+	va_start(ap, fmt);
+	size = vsnprintf(*ar + aroff, *n - aroff, fmt, ap);
+	va_end(ap);
+	return (ssize_t)size + aroff;
+}
+
+void
+dkim_err(struct message *msg, char *text)
+{
+	msg->err = 1;
+	fprintf(stderr, "%s: %s\n", text, strerror(errno));
+}
+
+void
+dkim_errx(struct message *msg, char *text)
+{
+	msg->err = 1;
+	fprintf(stderr, "%s\n", text);
+}
+
+__dead void
+usage(void)
+{
+	fprintf(stderr, "usage: filter-dkimverify\n");
+	exit(1);
+}
diff --git a/unpack_dns.c b/unpack_dns.c
new file mode 100644
index 0000000..8108226
--- /dev/null
+++ b/unpack_dns.c
@@ -0,0 +1,295 @@
+/*	$OpenBSD: unpack_dns.c,v 1.1 2018/01/06 07:57:53 sunil Exp $	*/
+
+/*
+ * Copyright (c) 2011-2014 Eric Faurot <eric@faurot.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <arpa/inet.h>
+
+#include <string.h>
+
+#include "unpack_dns.h"
+
+static int unpack_data(struct unpack *, void *, size_t);
+static int unpack_u16(struct unpack *, uint16_t *);
+static int unpack_u32(struct unpack *, uint32_t *);
+static int unpack_inaddr(struct unpack *, struct in_addr *);
+static int unpack_in6addr(struct unpack *, struct in6_addr *);
+static int unpack_dname(struct unpack *, char *, size_t);
+
+void
+unpack_init(struct unpack *unpack, const char *buf, size_t len)
+{
+	unpack->buf = buf;
+	unpack->len = len;
+	unpack->offset = 0;
+	unpack->err = NULL;
+}
+
+int
+unpack_header(struct unpack *p, struct dns_header *h)
+{
+	if (unpack_data(p, h, HFIXEDSZ) == -1)
+		return (-1);
+
+	h->flags = ntohs(h->flags);
+	h->qdcount = ntohs(h->qdcount);
+	h->ancount = ntohs(h->ancount);
+	h->nscount = ntohs(h->nscount);
+	h->arcount = ntohs(h->arcount);
+
+	return (0);
+}
+
+int
+unpack_query(struct unpack *p, struct dns_query *q)
+{
+	unpack_dname(p, q->q_dname, sizeof(q->q_dname));
+	unpack_u16(p, &q->q_type);
+	unpack_u16(p, &q->q_class);
+
+	return (p->err) ? (-1) : (0);
+}
+
+int
+unpack_rr(struct unpack *p, struct dns_rr *rr)
+{
+	uint16_t	rdlen;
+	size_t		save_offset;
+
+	unpack_dname(p, rr->rr_dname, sizeof(rr->rr_dname));
+	unpack_u16(p, &rr->rr_type);
+	unpack_u16(p, &rr->rr_class);
+	unpack_u32(p, &rr->rr_ttl);
+	unpack_u16(p, &rdlen);
+
+	if (p->err)
+		return (-1);
+
+	if (p->len - p->offset < rdlen) {
+		p->err = "too short";
+		return (-1);
+	}
+
+	save_offset = p->offset;
+
+	switch (rr->rr_type) {
+
+	case T_CNAME:
+		unpack_dname(p, rr->rr.cname.cname, sizeof(rr->rr.cname.cname));
+		break;
+
+	case T_MX:
+		unpack_u16(p, &rr->rr.mx.preference);
+		unpack_dname(p, rr->rr.mx.exchange, sizeof(rr->rr.mx.exchange));
+		break;
+
+	case T_NS:
+		unpack_dname(p, rr->rr.ns.nsname, sizeof(rr->rr.ns.nsname));
+		break;
+
+	case T_PTR:
+		unpack_dname(p, rr->rr.ptr.ptrname, sizeof(rr->rr.ptr.ptrname));
+		break;
+
+	case T_SOA:
+		unpack_dname(p, rr->rr.soa.mname, sizeof(rr->rr.soa.mname));
+		unpack_dname(p, rr->rr.soa.rname, sizeof(rr->rr.soa.rname));
+		unpack_u32(p, &rr->rr.soa.serial);
+		unpack_u32(p, &rr->rr.soa.refresh);
+		unpack_u32(p, &rr->rr.soa.retry);
+		unpack_u32(p, &rr->rr.soa.expire);
+		unpack_u32(p, &rr->rr.soa.minimum);
+		break;
+
+	case T_A:
+		if (rr->rr_class != C_IN)
+			goto other;
+		unpack_inaddr(p, &rr->rr.in_a.addr);
+		break;
+
+	case T_AAAA:
+		if (rr->rr_class != C_IN)
+			goto other;
+		unpack_in6addr(p, &rr->rr.in_aaaa.addr6);
+		break;
+	default:
+	other:
+		rr->rr.other.rdata = p->buf + p->offset;
+		rr->rr.other.rdlen = rdlen;
+		p->offset += rdlen;
+	}
+
+	if (p->err)
+		return (-1);
+
+	/* make sure that the advertised rdlen is really ok */
+	if (p->offset - save_offset != rdlen)
+		p->err = "bad dlen";
+
+	return (p->err) ? (-1) : (0);
+}
+
+ssize_t
+dname_expand(const unsigned char *data, size_t len, size_t offset,
+    size_t *newoffset, char *dst, size_t max)
+{
+	size_t		 n, count, end, ptr, start;
+	ssize_t		 res;
+
+	if (offset >= len)
+		return (-1);
+
+	res = 0;
+	end = start = offset;
+
+	for (; (n = data[offset]); ) {
+		if ((n & 0xc0) == 0xc0) {
+			if (offset + 2 > len)
+				return (-1);
+			ptr = 256 * (n & ~0xc0) + data[offset + 1];
+			if (ptr >= start)
+				return (-1);
+			if (end < offset + 2)
+				end = offset + 2;
+			offset = start = ptr;
+			continue;
+		}
+		if (offset + n + 1 > len)
+			return (-1);
+
+		/* copy n + at offset+1 */
+		if (dst != NULL && max != 0) {
+			count = (max < n + 1) ? (max) : (n + 1);
+			memmove(dst, data + offset, count);
+			dst += count;
+			max -= count;
+		}
+		res += n + 1;
+		offset += n + 1;
+		if (end < offset)
+			end = offset;
+	}
+	if (end < offset + 1)
+		end = offset + 1;
+
+	if (dst != NULL && max != 0)
+		dst[0] = 0;
+	if (newoffset)
+		*newoffset = end;
+	return (res + 1);
+}
+
+char *
+print_dname(const char *_dname, char *buf, size_t max)
+{
+	const unsigned char *dname = _dname;
+	char    *res;
+	size_t   left, count;
+
+	if (_dname[0] == 0) {
+		(void)strlcpy(buf, ".", max);
+		return buf;
+	}
+
+	res = buf;
+	left = max - 1;
+	for (; dname[0] && left;) {
+		count = (dname[0] < (left - 1)) ? dname[0] : (left - 1);
+		memmove(buf, dname + 1, count);
+		dname += dname[0] + 1;
+		left -= count;
+		buf += count;
+		if (left) {
+			left -= 1;
+			*buf++ = '.';
+		}
+	}
+	buf[0] = 0;
+
+	return (res);
+}
+
+static int
+unpack_data(struct unpack *p, void *data, size_t len)
+{
+	if (p->err)
+		return (-1);
+
+	if (p->len - p->offset < len) {
+		p->err = "too short";
+		return (-1);
+	}
+
+	memmove(data, p->buf + p->offset, len);
+	p->offset += len;
+
+	return (0);
+}
+
+static int
+unpack_u16(struct unpack *p, uint16_t *u16)
+{
+	if (unpack_data(p, u16, 2) == -1)
+		return (-1);
+
+	*u16 = ntohs(*u16);
+
+	return (0);
+}
+
+static int
+unpack_u32(struct unpack *p, uint32_t *u32)
+{
+	if (unpack_data(p, u32, 4) == -1)
+		return (-1);
+
+	*u32 = ntohl(*u32);
+
+	return (0);
+}
+
+static int
+unpack_inaddr(struct unpack *p, struct in_addr *a)
+{
+	return (unpack_data(p, a, 4));
+}
+
+static int
+unpack_in6addr(struct unpack *p, struct in6_addr *a6)
+{
+	return (unpack_data(p, a6, 16));
+}
+
+static int
+unpack_dname(struct unpack *p, char *dst, size_t max)
+{
+	ssize_t e;
+
+	if (p->err)
+		return (-1);
+
+	e = dname_expand(p->buf, p->len, p->offset, &p->offset, dst, max);
+	if (e == -1) {
+		p->err = "bad domain name";
+		return (-1);
+	}
+	if (e < 0 || e > MAXDNAME) {
+		p->err = "domain name too long";
+		return (-1);
+	}
+
+	return (0);
+}
diff --git a/unpack_dns.h b/unpack_dns.h
new file mode 100644
index 0000000..2318a0c
--- /dev/null
+++ b/unpack_dns.h
@@ -0,0 +1,96 @@
+/*	$OpenBSD: unpack_dns.h,v 1.1 2018/01/06 07:57:53 sunil Exp $	*/
+
+/*
+ * Copyright (c) 2011-2014 Eric Faurot <eric@faurot.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include <netinet/in.h>
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+
+struct unpack {
+	const char	*buf;
+	size_t		 len;
+	size_t		 offset;
+	const char	*err;
+};
+
+struct dns_header {
+	uint16_t	id;
+	uint16_t	flags;
+	uint16_t	qdcount;
+	uint16_t	ancount;
+	uint16_t	nscount;
+	uint16_t	arcount;
+};
+
+struct dns_query {
+	char		q_dname[MAXDNAME];
+	uint16_t	q_type;
+	uint16_t	q_class;
+};
+
+struct dns_rr {
+	char		rr_dname[MAXDNAME];
+	uint16_t	rr_type;
+	uint16_t	rr_class;
+	uint32_t	rr_ttl;
+	union {
+		struct {
+			char	cname[MAXDNAME];
+		} cname;
+		struct {
+			uint16_t	preference;
+			char		exchange[MAXDNAME];
+		} mx;
+		struct {
+			char	nsname[MAXDNAME];
+		} ns;
+		struct {
+			char	ptrname[MAXDNAME];
+		} ptr;
+		struct {
+			char		mname[MAXDNAME];
+			char		rname[MAXDNAME];
+			uint32_t	serial;
+			uint32_t	refresh;
+			uint32_t	retry;
+			uint32_t	expire;
+			uint32_t	minimum;
+		} soa;
+		struct {
+			struct in_addr	addr;
+		} in_a;
+		struct {
+			struct in6_addr	addr6;
+		} in_aaaa;
+		struct {
+			uint16_t	 rdlen;
+			const void	*rdata;
+		} other;
+	} rr;
+};
+
+void	 unpack_init(struct unpack *, const char *, size_t);
+int	 unpack_header(struct unpack *, struct dns_header *);
+int	 unpack_rr(struct unpack *, struct dns_rr *);
+int	 unpack_query(struct unpack *, struct dns_query *);
+char    *print_dname(const char *, char *, size_t);
+ssize_t	 dname_expand(const unsigned char *, size_t, size_t, size_t *,
+	    char *, size_t);
+