Commit Diff


commit - 7e7046817041529117a0aa3b446e48c43a50619f
commit + 583f078afc6d1149d45e76165796bc612acb204a
blob - e032c9d28e7f4b33bfa138564c4c8e1f9af047bb
blob + 4bb8f0e5412d730fc95f34d2be25a661732b4534
--- filter-auth.8
+++ filter-auth.8
@@ -20,14 +20,65 @@
 .Os
 .Sh NAME
 .Nm filter-auth
-.Nd authenticate email via verification of dkim signature
+.Nd authenticate email and add Authentication-Results header
 .Sh SYNOPSIS
 .Nm
+.Op Fl A
+.Op Ar authserv-id
 .Sh DESCRIPTION
 .Nm
-verifies the dkim signatures of messages and adds an Authentication-Results
-header to the message.
+verifies the DKIM and ARC signatures of messages, the SPF policy and
+adds an Authentication-Results header to the message. If the
+.Fl A
+flag is specified, it adds the ARC-Authentication-Results header
+instead, with the next unused
+.Em i
+value in the chain. This filter gets used
+.Em authserv-id
+from OpenSMTPD which can be overridden by the
+.Ar authserv-id
+argument.
 .Sh SEE ALSO
 .Xr filter-admdscrub 8
 .Xr filter-dkimsign 8
 .Xr smtpd 8
+.Sh STANDARDS
+.\" RFC7208
+.\" RFC8601
+.Rs
+.%A J. Klensin
+.%D October 2008
+.%R RFC 5321
+.%T Simple Mail Transfer Protocol
+.Re
+.Pp
+.Rs
+.%A D. Crocker, Ed.
+.%Q Brandenburg InternetWorking
+.%A T. Hansen, Ed.
+.%Q AT&T Laboratories
+.%A M. Kucherawy, Ed.
+.%Q Cloudmark
+.%D September 2011
+.%R RFC 6376
+.%T DomainKeys Identified Mail (DKIM) Signatures
+.Re
+.Pp
+.Rs
+.%A S. Kitterman
+.%Q Kitterman Technical Services
+.%D April 2014
+.%R RFC 7208
+.%T Sender Policy Framework (SPF) for Authorizing Use of Domains in Email, Version 1
+.Re
+.Pp
+.Rs
+.%A M. Kucherawy
+.%D May 2019
+.%R RFC 8601
+.%T Message Header Field for Indicating Message Authentication Status
+.Re
+.Sh AUTHORS
+.An Kirill A. Korinsky Aq Mt kirill@korins.ky
+.An Martijn van Duren Aq Mt martijn@openbsd.org
+.An Gilles Chehade Aq Mt gilles@poolp.org
blob - 33546d36fa9c8e45e9c30423b18e5cb446de5f59
blob + d0c64a4f1761f041ffd22d8623fafb13cc4ef81c
--- main.c
+++ main.c
@@ -65,6 +65,8 @@ struct ar_signature {
 	struct header *header;
 	enum ar_state state;
 	const char *state_reason;
+	int dkim;
+	int seal;
 	int v;
 	const char *a;
 	size_t asz;
@@ -82,6 +84,7 @@ struct ar_signature {
 	size_t bhsz;
 	EVP_MD_CTX *bhctx;
 	int c;
+	enum ar_state cv;
 #define CANON_HEADER_SIMPLE	0
 #define CANON_HEADER_RELAXED	1
 #define CANON_HEADER		1
@@ -91,6 +94,7 @@ struct ar_signature {
 #define CANON_DONE		1 << 2
 	char d[HOST_NAME_MAX + 1];
 	char **h;
+	int arc_i;
 	const char *i;
 	size_t isz;
 	ssize_t l;
@@ -156,6 +160,11 @@ struct header {
 #define AUTHENTICATION_RESULTS_LINELEN 78
 #define MIN(a, b) ((a) < (b) ? (a) : (b))
 
+
+/* RFC 8617 Section 4.2.1 */
+#define ARC_MIN_I 1
+#define ARC_MAX_I 50
+
 struct message {
 	struct osmtpd_ctx *ctx;
 	FILE *origf;
@@ -166,6 +175,10 @@ struct message {
 	size_t nheaders;
 	int readdone;
 	int nqueries;
+	struct ar_signature *last_arc_seal;
+	struct ar_signature *last_arc_sign;
+	struct ar_signature **arc_seals;
+	struct ar_signature **arc_signs;
 };
 
 struct session {
@@ -191,15 +204,17 @@ void auth_session_free(struct osmtpd_ctx *, void *);
 void *auth_message_new(struct osmtpd_ctx *);
 void auth_message_free(struct osmtpd_ctx *, void *);
 void ar_header_add(struct osmtpd_ctx *, const char *);
-void ar_signature_parse(struct header *);
+void ar_signature_parse(struct header *, int, int);
 void ar_signature_parse_v(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_a(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_b(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_bh(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_c(struct ar_signature *, const char *, const char *);
+void arc_signature_parse_cv(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_d(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_h(struct ar_signature *, const char *, const char *);
-void ar_signature_parse_i(struct ar_signature *, const char *, const char *);
+void dkim_signature_parse_i(struct ar_signature *, const char *, const char *);
+void arc_signature_parse_i(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_l(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_q(struct ar_signature *, const char *, const char *);
 void ar_signature_parse_s(struct ar_signature *, const char *, const char *);
@@ -232,25 +247,50 @@ int spf_execute_txt(struct spf_query *);
 int spf_ar_cat(const char *, struct spf_record *, char **, size_t *, ssize_t *);
 void auth_message_verify(struct message *);
 void auth_ar_create(struct osmtpd_ctx *);
+int ar_signature_ar_cat(const char *, struct ar_signature *, char **, size_t *, ssize_t *);
 ssize_t auth_ar_cat(char **ar, size_t *n, size_t aroff, const char *fmt, ...)
     __attribute__((__format__ (printf, 4, 5)));
 int auth_ar_print(struct osmtpd_ctx *, const char *);
 int ar_key_text_parse(struct ar_signature *, const char *);
 
 
-char *authservid;
+/* RFC8617 Section 5.1.1 */
+static char *arc_seal_headers[] = {
+	"ARC-Authentication-Results",
+	"ARC-Message-Signature",
+	"ARC-Seal"
+};
+
+char *authservid = NULL;
+int arc = 0;
 EVP_ENCODE_CTX *ectx = NULL;
 
 int
 main(int argc, char *argv[])
 {
-	if (argc != 1)
-		osmtpd_errx(1, "Invalid argument count");
+	int ch;
 
 	OpenSSL_add_all_digests();
 
 	if (pledge("tmppath stdio dns", NULL) == -1)
 		osmtpd_err(1, "pledge");
+
+	while ((ch = getopt(argc, argv, "A")) != -1) {
+		switch (ch) {
+		case 'A':
+			arc = 1;
+			break;
+		default:
+			usage();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+	if (argc > 1)
+		osmtpd_errx(1, "invalid authservid count");
+	if (argc == 1)
+		authservid = argv[0];
 
 	if ((ectx = EVP_ENCODE_CTX_new()) == NULL)
 		osmtpd_err(1, "EVP_ENCODE_CTX_new");
@@ -523,6 +563,14 @@ auth_message_new(struct osmtpd_ctx *ctx)
 	msg->nheaders = 0;
 	msg->readdone = 0;
 	msg->nqueries = 0;
+	msg->last_arc_seal = NULL;
+	msg->last_arc_sign = NULL;
+	if ((msg->arc_seals =
+	    calloc(ARC_MAX_I + 1, sizeof(*msg->arc_seals))) == NULL)
+		osmtpd_err(1, "%s: malloc", __func__);
+	if ((msg->arc_signs =
+	    calloc(ARC_MAX_I + 1, sizeof(*msg->arc_signs))) == NULL)
+		osmtpd_err(1, "%s: malloc", __func__);
 
 	return msg;
 }
@@ -538,10 +586,12 @@ auth_message_free(struct osmtpd_ctx *ctx, void *data)
 		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);
+			if (msg->header[i].sig->h != arc_seal_headers) {
+				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);
 			if (msg->header[i].sig->query)
 				event_asr_abort(msg->header[i].sig->query);
@@ -550,6 +600,8 @@ auth_message_free(struct osmtpd_ctx *ctx, void *data)
 		free(msg->header[i].sig);
 	}
 	free(msg->header);
+	free(msg->arc_seals);
+	free(msg->arc_signs);
 	free(msg);
 }
 
@@ -575,7 +627,20 @@ ar_header_add(struct osmtpd_ctx *ctx, const char *line
 			    start, "DKIM-Signature", end - start) == 0 &&
 			    verify[0] == ':')
 				ar_signature_parse(
-				    &msg->header[msg->nheaders - 1]);
+				    &msg->header[msg->nheaders - 1], 1, 0);
+			else if (end != NULL &&
+			    strncasecmp(
+			    start, "ARC-Message-Signature", end - start) == 0 &&
+			    verify[0] == ':')
+				ar_signature_parse(
+				    &msg->header[msg->nheaders - 1], 0, 0);
+			else if (end != NULL &&
+			    strncasecmp(
+			    start, "ARC-Seal", end - start) == 0 &&
+			    verify[0] == ':')
+				ar_signature_parse(
+				    &msg->header[msg->nheaders - 1], 0, 1);
+
 			if (line[0] == '\0')
 				return;
 		} else {
@@ -620,9 +685,9 @@ ar_header_cat(struct osmtpd_ctx *ctx, const char *line
 }
 
 void
-ar_signature_parse(struct header *header)
+ar_signature_parse(struct header *header, int dkim, int seal)
 {
-	struct ar_signature *sig;
+	struct ar_signature *sig, *last;
 	const char *buf, *i, *end;
 	char tagname[3];
 	char subdomain[HOST_NAME_MAX + 1];
@@ -636,6 +701,8 @@ ar_signature_parse(struct header *header)
 		osmtpd_err(1, "%s: malloc", __func__);
 	sig = header->sig;
 	sig->header = header;
+	sig->dkim = dkim;
+	sig->seal = seal;
 	sig->l = -1;
 	sig->t = -1;
 	sig->x = -1;
@@ -659,33 +726,37 @@ ar_signature_parse(struct header *header)
 		/* '=' */
 		buf = osmtpd_ltok_skip_fws(buf + 1, 1);
 		end = osmtpd_ltok_skip_tag_value(buf, 1);
-		if (strcmp(tagname, "v") == 0)
+		if (dkim && strcmp(tagname, "v") == 0)
 			ar_signature_parse_v(sig, buf, end);
 		else if (strcmp(tagname, "a") == 0)
 			ar_signature_parse_a(sig, buf, end);
 		else if (strcmp(tagname, "b") == 0)
 			ar_signature_parse_b(sig, buf, end);
-		else if (strcmp(tagname, "bh") == 0)
+		else if (!seal && strcmp(tagname, "bh") == 0)
 			ar_signature_parse_bh(sig, buf, end);
-		else if (strcmp(tagname, "c") == 0)
+		else if (!seal && strcmp(tagname, "c") == 0)
 			ar_signature_parse_c(sig, buf, end);
+		else if (seal && strcmp(tagname, "cv") == 0)
+			arc_signature_parse_cv(sig, buf, end);
 		else if (strcmp(tagname, "d") == 0)
 			ar_signature_parse_d(sig, buf, end);
-		else if (strcmp(tagname, "h") == 0)
+		else if (!seal && strcmp(tagname, "h") == 0)
 			ar_signature_parse_h(sig, buf, end);
-		else if (strcmp(tagname, "i") == 0)
-			ar_signature_parse_i(sig, buf, end);
-		else if (strcmp(tagname, "l") == 0)
+		else if (dkim && strcmp(tagname, "i") == 0)
+			dkim_signature_parse_i(sig, buf, end);
+		else if (!dkim && strcmp(tagname, "i") == 0)
+			arc_signature_parse_i(sig, buf, end);
+		else if (!seal && strcmp(tagname, "l") == 0)
 			ar_signature_parse_l(sig, buf, end);
-		else if (strcmp(tagname, "q") == 0)
+		else if (!seal && strcmp(tagname, "q") == 0)
 			ar_signature_parse_q(sig, buf, end);
 		else if (strcmp(tagname, "s") == 0)
 			ar_signature_parse_s(sig, buf, end);
 		else if (strcmp(tagname, "t") == 0)
 			ar_signature_parse_t(sig, buf, end);
-		else if (strcmp(tagname, "x") == 0)
+		else if (!seal && strcmp(tagname, "x") == 0)
 			ar_signature_parse_x(sig, buf, end);
-		else if (strcmp(tagname, "z") == 0)
+		else if (!seal && strcmp(tagname, "z") == 0)
 			ar_signature_parse_z(sig, buf, end);
 
 		buf = osmtpd_ltok_skip_fws(end, 1);
@@ -700,23 +771,32 @@ ar_signature_parse(struct header *header)
 	if (sig->state != AR_UNKNOWN)
 		return;
 
-	if (sig->v != 1)
+	if (dkim && sig->v != 1)
 		ar_signature_state(sig, AR_PERMERROR, "Missing v tag");
 	else if (sig->ah == NULL)
 		ar_signature_state(sig, AR_PERMERROR, "Missing a tag");
 	else if (sig->b == NULL)
 		ar_signature_state(sig, AR_PERMERROR, "Missing b tag");
-	else if (sig->bhsz == 0)
+	else if (!seal && sig->bhsz == 0)
 		ar_signature_state(sig, AR_PERMERROR, "Missing bh tag");
+	else if (seal && sig->cv == AR_UNKNOWN)
+		ar_signature_state(sig, AR_PERMERROR, "Missing cv tag");
 	else if (sig->d[0] == '\0')
 		ar_signature_state(sig, AR_PERMERROR, "Missing d tag");
-	else if (sig->h == NULL)
+	else if (!dkim && sig->arc_i == 0)
+		ar_signature_state(sig, AR_PERMERROR, "Missing i tag");
+	else if (!seal && sig->h == NULL)
 		ar_signature_state(sig, AR_PERMERROR, "Missing h tag");
 	else if (sig->s[0] == '\0')
 		ar_signature_state(sig, AR_PERMERROR, "Missing s tag");
 	if (sig->state != AR_UNKNOWN)
 		return;
 
+	if (seal) {
+		sig->c = CANON_HEADER_RELAXED;
+		sig->h = arc_seal_headers;
+	}
+
 	if (sig->i != NULL) {
 		i = osmtpd_ltok_skip_local_part(sig->i, 1) + 1;
 		ilen = sig->isz - (size_t)(i - sig->i);
@@ -739,6 +819,41 @@ ar_signature_parse(struct header *header)
 		return;
 	}
 
+	if (!dkim) {
+		if (seal) {
+			last = header->msg->last_arc_seal;
+			header->msg->last_arc_seal = sig;
+			if (header->msg->arc_seals[sig->arc_i] == NULL)
+				header->msg->arc_seals[sig->arc_i] = sig;
+		} else {
+			last = header->msg->last_arc_sign;
+			header->msg->last_arc_sign = sig;
+			if (header->msg->arc_signs[sig->arc_i] == NULL)
+				header->msg->arc_signs[sig->arc_i] = sig;
+		}
+
+		if (last != NULL) {
+			if ((last->arc_i - 1) != sig->arc_i) {
+				ar_signature_state(
+				    last, AR_PERMERROR, "Invalind i-chain");
+				return;
+			}
+
+			switch (last->cv) {
+			case AR_UNKNOWN:
+			case AR_FAIL:
+				break;
+			case AR_PASS:
+				if (sig->cv == AR_PASS)
+					break;
+			default:
+				ar_signature_state(
+				    last, AR_PERMERROR, "Invalind cv-chain");
+				return;
+			}
+		}
+	}
+
 	if ((size_t)snprintf(subdomain, sizeof(subdomain), "%s._domainkey.%s",
 	    sig->s, sig->d) >= sizeof(subdomain)) {
 		ar_signature_state(sig, AR_PERMERROR,
@@ -968,6 +1083,26 @@ ar_signature_parse_c(struct ar_signature *sig, const c
 }
 
 void
+arc_signature_parse_cv(struct ar_signature *sig, const char *start, const char *end)
+{
+	if (sig->cv != AR_UNKNOWN) {
+		ar_signature_state(sig, AR_PERMERROR, "Duplicate cv tag");
+		return;
+	}
+
+	if (strncmp(start, "pass", 4) == 0) {
+		sig->cv = AR_PASS;
+	} else if (strncmp(start, "fail", 4) == 0) {
+		sig->cv = AR_FAIL;
+	} else if (strncmp(start, "none", 4) == 0) {
+		sig->cv = AR_NONE;
+	} else {
+		ar_signature_state(sig, AR_PERMERROR, "Invalid cv tag");
+		return;
+	}
+}
+
+void
 ar_signature_parse_d(struct ar_signature *sig, const char *start, const char *end)
 {
 	if (sig->d[0] != '\0') {
@@ -1035,7 +1170,7 @@ ar_signature_parse_h(struct ar_signature *sig, const c
 }
 
 void
-ar_signature_parse_i(struct ar_signature *sig, const char *start, const char *end)
+dkim_signature_parse_i(struct ar_signature *sig, const char *start, const char *end)
 {
 	if (sig->i != NULL) {
 		ar_signature_state(sig, AR_PERMERROR, "Duplicate i tag");
@@ -1050,6 +1185,27 @@ ar_signature_parse_i(struct ar_signature *sig, const c
 }
 
 void
+arc_signature_parse_i(struct ar_signature *sig, const char *start, const char *end)
+{
+	char *ep;
+	long i;
+	if (sig->arc_i != 0) {
+		ar_signature_state(sig, AR_PERMERROR, "Duplicate i tag");
+		return;
+	}
+	if (osmtpd_ltok_skip_digit(start, 0) != end) {
+		ar_signature_state(sig, AR_PERMERROR, "Invalid i tag");
+		return;
+	}
+	i = strtol(start, &ep, 10);
+	if (i < ARC_MIN_I || i > ARC_MAX_I || ep != end) {
+		ar_signature_state(sig, AR_PERMERROR, "Invalid i tag");
+		return;
+	}
+	sig->arc_i = (int) i;
+}
+
+void
 ar_signature_parse_l(struct ar_signature *sig, const char *start, const char *end)
 {
 	long long l;
@@ -1186,6 +1342,13 @@ ar_signature_verify(struct ar_signature *sig)
 	if (sig->state != AR_UNKNOWN)
 		return;
 
+	if (sig->cv == AR_FAIL ||
+	    (sig->cv == AR_PASS && sig->arc_i == 1) ||
+	    (sig->cv == AR_NONE && sig->arc_i > 1)) {
+		ar_signature_state(sig, AR_FAIL, "cv tag");
+		return;
+	}
+
 	if (bctx == NULL) {
 		if ((bctx = EVP_MD_CTX_new()) == NULL)
 			osmtpd_err(1, "EVP_MD_CTX_new");
@@ -1207,27 +1370,32 @@ ar_signature_verify(struct ar_signature *sig)
 	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;
-			ar_signature_header(bctx, sig, &(msg->header[i]));
-			msg->header[i].parsed = 1;
-			break;
+	if (sig->h != NULL) {
+		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;
+				ar_signature_header(bctx, sig, &(msg->header[i]));
+				msg->header[i].parsed = 1;
+				break;
+			}
 		}
 	}
+
 	ar_signature_header(bctx, sig, sig->header);
 	if (!sig->sephash) {
-		if (EVP_DigestVerifyFinal(bctx, sig->b, sig->bsz) != 1)
+		if (EVP_DigestVerifyFinal(bctx, sig->b, sig->bsz) != 1) {
 			ar_signature_state(sig, AR_FAIL, "b mismatch");
+			return;
+		}
 	} else {
 		if (EVP_DigestFinal_ex(bctx, digest, &digestsz) == 0)
 			osmtpd_err(1, "EVP_DigestFinal_ex");
@@ -1241,11 +1409,30 @@ ar_signature_verify(struct ar_signature *sig)
 			break;
 		case 0:
 			ar_signature_state(sig, AR_FAIL, "b mismatch");
-			break;
+			return;
 		default:
 			osmtpd_err(1, "EVP_DigestVerify");
 		}
 	}
+
+	if (sig->arc_i > 0) {
+		if (msg->arc_seals[sig->arc_i] == NULL ||
+		    msg->arc_signs[sig->arc_i] == NULL) {
+			ar_signature_state(sig, AR_PERMERROR, "missed ARC header");
+			return;
+		}
+
+		if (msg->arc_seals[sig->arc_i]->state == AR_UNKNOWN ||
+		    msg->arc_signs[sig->arc_i]->state == AR_UNKNOWN)
+			return;
+
+		if (msg->arc_seals[sig->arc_i]->state !=
+		    msg->arc_signs[sig->arc_i]->state) {
+			ar_signature_state(
+			    msg->arc_signs[sig->arc_i], AR_FAIL, NULL);
+			return;
+		}
+	}
 }
 
 /* EVP_DigestVerifyUpdate is a macro, so we can't alias this on a variable */
@@ -1320,11 +1507,15 @@ ar_signature_state(struct ar_signature *sig, enum ar_s
 	}
 	switch (sig->state) {
 	case AR_UNKNOWN:
+	case AR_NONE:
 		break;
-	case AR_PASS:
 	case AR_FAIL:
+		if (state == AR_PERMERROR)
+			break;
+	case AR_PASS:
 	case AR_SOFTFAIL:
-		osmtpd_errx(1, "Unexpected transition");
+		osmtpd_errx(1, "Unexpected transition: %s -> %",
+		    ar_state2str(sig->state), ar_state2str(state));
 	case AR_POLICY:
 		if (state == AR_PASS)
 			return;
@@ -1334,7 +1525,8 @@ ar_signature_state(struct ar_signature *sig, enum ar_s
 			return;
 		if (state == AR_TEMPERROR || state == AR_PERMERROR)
 			break;
-		osmtpd_errx(1, "Unexpected transition");
+		osmtpd_errx(1, "Unexpected transition: %s -> %",
+		    ar_state2str(sig->state), ar_state2str(state));
 	case AR_TEMPERROR:
 		if (state == AR_PERMERROR)
 			break;
@@ -1353,6 +1545,8 @@ ar_state2str(enum ar_state state)
 	{
 	case AR_UNKNOWN:
 		return "unknown";
+	case AR_NONE:
+		return "none";
 	case AR_PASS:
 		return "pass";
 	case AR_FAIL:
@@ -1803,6 +1997,9 @@ ar_body_verify(struct ar_signature *sig)
 	if (sig->state != AR_UNKNOWN)
 		return;
 
+	if (sig->seal)
+		return;
+
 	if ((sig->c & CANON_BODY) == CANON_BODY_SIMPLE &&
 	    !sig->header->msg->has_body) {
 		if (EVP_DigestUpdate(sig->bhctx, "\r\n",
@@ -2732,7 +2929,72 @@ auth_message_verify(struct message *msg)
 
 	auth_ar_create(msg->ctx);
 }
+
+int
+ar_signature_ar_cat(const char *type, struct ar_signature *sig, char **line, size_t *linelen, ssize_t *aroff)
+{
+	if ((*aroff =
+	    auth_ar_cat(line, linelen, *aroff,
+	    "; %s=%s", type, ar_state2str(sig->state))
+	    ) == -1)
+		return -1;
+
+	if (sig->state_reason != NULL) {
+		if ((*aroff =
+		    auth_ar_cat(line, linelen, *aroff,
+		    " reason=\"%s\"", sig->state_reason)
+		    ) == -1)
+			return -1;
+	}
+
+	if (sig->s[0] != '\0') {
+		if ((*aroff =
+		    auth_ar_cat(line, linelen, *aroff,
+		    " header.s=%s", sig->s)
+		    ) == -1)
+			return -1;
+	}
 
+	if (sig->d[0] != '\0') {
+		if ((*aroff =
+		    auth_ar_cat(line, linelen, *aroff,
+		    " header.d=%s", sig->d)
+		    ) == -1)
+			return -1;
+	}
+
+	/*
+	 * Don't print i-tag for DKIM, since localpart can be a
+	 * quoted-string, which can contain FWS and CFWS. But
+	 * ARC is different story and it should be printed out.
+	 */
+	if (sig->arc_i != 0) {
+		if ((*aroff =
+		    auth_ar_cat(line, linelen, *aroff,
+		    " header.i=%d", sig->arc_i)
+		    ) == -1)
+			return -1;
+	}
+
+	if (sig->a != NULL) {
+		if ((*aroff =
+		    auth_ar_cat(line, linelen, *aroff,
+		    " header.a=%.*s", (int)sig->asz, sig->a)
+		    ) == -1)
+			return -1;
+	}
+
+	if (sig->bheaderclean[0] != '\0') {
+		if ((*aroff =
+		    auth_ar_cat(line, linelen, *aroff,
+		    " header.b=%s", sig->bheaderclean)
+		    ) == -1)
+			return -1;
+	}
+
+	return 0;
+}
+
 void
 auth_ar_create(struct osmtpd_ctx *ctx)
 {
@@ -2745,53 +3007,64 @@ auth_ar_create(struct osmtpd_ctx *ctx)
 	struct session *ses = ctx->local_session;
 	struct message *msg = ctx->local_message;
 
-	if ((aroff = auth_ar_cat(&line, &linelen, aroff,
+	if (!arc && (aroff = auth_ar_cat(&line, &linelen, aroff,
 	    "Authentication-Results: %s", authservid)) == -1)
 		osmtpd_err(1, "%s: malloc", __func__);
+
+	if (arc) {
+		for (i = ARC_MAX_I; i >= ARC_MIN_I; i--) {
+			if (msg->arc_signs[i] != NULL)
+				break;
+		}
+		i += 1;
+
+		if (i <= ARC_MAX_I && (aroff = auth_ar_cat(
+		    &line, &linelen, aroff,
+		    "ARC-Authentication-Results: i=%zu; %s",
+		    i, authservid)) == -1)
+			osmtpd_err(1, "%s: malloc", __func__);
+	}
+
 	for (i = 0; i < msg->nheaders; i++) {
 		sig = msg->header[i].sig;
-		if (sig == NULL)
+		if (sig == NULL || !sig->dkim)
 			continue;
+
 		found = 1;
-		if ((aroff = auth_ar_cat(&line, &linelen, aroff, "; dkim=%s",
-		    ar_state2str(sig->state))) == -1)
+
+		if (ar_signature_ar_cat(
+		    "dkim", sig, &line, &linelen, &aroff) != 0)
 			osmtpd_err(1, "%s: malloc", __func__);
-		if (sig->state_reason != NULL) {
-			if ((aroff = auth_ar_cat(&line, &linelen, aroff,
-			    " reason=\"%s\"", sig->state_reason)) == -1)
-				osmtpd_err(1, "%s: malloc", __func__);
-		}
-		if (sig->s[0] != '\0') {
-			if ((aroff = auth_ar_cat(&line, &linelen, aroff,
-			    " header.s=%s", sig->s)) == -1)
-				osmtpd_err(1, "%s: malloc", __func__);
-		}
-		if (sig->d[0] != '\0') {
-			if ((aroff = auth_ar_cat(&line, &linelen, aroff,
-			    " header.d=%s", sig->d)) == -1)
-				osmtpd_err(1, "%s: malloc", __func__);
-		}
-		/*
-		 * Don't print i-tag, since localpart can be a quoted-string,
-		 * which can contain FWS and CFWS.
-		 */
-		if (sig->a != NULL) {
-			if ((aroff = auth_ar_cat(&line, &linelen, aroff,
-			    " header.a=%.*s", (int)sig->asz, sig->a)) == -1)
-				osmtpd_err(1, "%s: malloc", __func__);
-		}
-		if (sig->bheaderclean[0] != '\0') {
-			if ((aroff = auth_ar_cat(&line, &linelen, aroff,
-			    " header.b=%s", sig->bheaderclean)) == -1)
-				osmtpd_err(1, "%s: malloc", __func__);
-		}
 	}
+
 	if (!found) {
 		aroff = auth_ar_cat(&line, &linelen, aroff, "; dkim=none");
 		if (aroff == -1)
 			osmtpd_err(1, "%s: malloc", __func__);
 	}
 
+	found = 0;
+
+	for (i = ARC_MAX_I; i > 0; i--) {
+		sig = msg->arc_signs[i];
+		if (sig == NULL)
+			continue;
+
+		found = 1;
+
+		if (ar_signature_ar_cat(
+		    "arc", sig, &line, &linelen, &aroff) != 0)
+			osmtpd_err(1, "%s: malloc", __func__);
+
+		break;
+	}
+
+	if (!found) {
+		aroff = auth_ar_cat(&line, &linelen, aroff, "; arc=none");
+		if (aroff == -1)
+			osmtpd_err(1, "%s: malloc", __func__);
+	}
+
 	if ((aroff = auth_ar_cat(&line, &linelen, aroff,
 	    "; iprev=%s", ar_state2str(ses->iprev))) == -1)
 		osmtpd_err(1, "%s: malloc", __func__);
@@ -2859,12 +3132,21 @@ auth_ar_print(struct osmtpd_ctx *ctx, const char *star
 		if (scan == ncheckpoint) {
 			checkpoint = ncheckpoint;
 			ncheckpoint = osmtpd_ltok_skip_cfws(ncheckpoint, 1);
+			/* ARC-AR starts with i= */
+			if (strncmp(ncheckpoint, "i=",
+			    sizeof("i=") - 1) == 0) {
+				ncheckpoint = osmtpd_ltok_skip_digit(
+				    ncheckpoint + sizeof("i=") - 1, 0);
 			/* authserv-id */
-			if (arid) {
+			} else if (arid) {
 				ncheckpoint = osmtpd_ltok_skip_value(
 				    ncheckpoint, 0);
 				arid = 0;
 			/* methodspec */
+			} else if (strncmp(ncheckpoint, "arc=",
+			    sizeof("arc=") - 1) == 0) {
+				ncheckpoint = osmtpd_ltok_skip_keyword(
+				    ncheckpoint + sizeof("arc=") - 1, 0);
 			} else if (strncmp(ncheckpoint, "dkim=",
 			    sizeof("dkim=") - 1) == 0) {
 				ncheckpoint = osmtpd_ltok_skip_keyword(
@@ -2931,6 +3213,6 @@ auth_ar_cat(char **ar, size_t *n, size_t aroff, const 
 __dead void
 usage(void)
 {
-	fprintf(stderr, "usage: filter-auth\n");
+	fprintf(stderr, "usage: filter-auth [-A] [authserv-id]\n");
 	exit(1);
 }