* Exim - an Internet mail transport agent *
*************************************************/
/* Experimental ARC support for Exim
- Copyright (c) Jeremy Harris 2018
+ Copyright (c) Jeremy Harris 2018 - 2020
+ Copyright (c) The Exim Maintainers 2021 - 2023
License: GPL
+ SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "exim.h"
-#ifdef EXPERIMENTAL_ARC
-# if !defined SUPPORT_SPF
-# error SPF must also be enabled for ARC
-# elif defined DISABLE_DKIM
+#if defined EXPERIMENTAL_ARC
+# if defined DISABLE_DKIM
# error DKIM must also be enabled for ARC
# else
# include "pdkim/pdkim.h"
# include "pdkim/signing.h"
+# ifdef SUPPORT_DMARC
+# include "dmarc.h"
+# endif
+
extern pdkim_ctx * dkim_verify_ctx;
extern pdkim_ctx dkim_sign_ctx;
+#define ARC_SIGN_OPT_TSTAMP BIT(0)
+#define ARC_SIGN_OPT_EXPIRE BIT(1)
+
+#define ARC_SIGN_DEFAULT_EXPIRE_DELTA (60 * 60 * 24 * 30) /* one month */
+
/******************************************************************************/
typedef struct hdr_rlist {
blob s;
blob c;
blob l;
+ blob ip;
/* tag content sub-portions */
blob a_algo;
#define HDR_AR US"Authentication-Results:"
#define HDRLEN_AR 23
+typedef enum line_extract {
+ le_instance_only,
+ le_instance_plus_ip,
+ le_all
+} line_extract_t;
+
+static time_t now;
+static time_t expire;
static hdr_rlist * headers_rlist;
static arc_ctx arc_sign_ctx = { NULL };
+static arc_ctx arc_verify_ctx = { NULL };
+
+/* We build a context for either Sign or Verify.
+
+For Verify, it's a fresh new one for ACL verify=arc - there is no connection
+with the single line handling done during reception via the DKIM feed.
+
+For Verify we do it twice; initially during reception (via the DKIM feed)
+and then later for the full verification.
+
+The former only looks at AMS headers, to discover what hash(es) we need done for
+ARC on the message body; we call back to the DKIM code to set up so that it does
+them for us during reception. That call needs info from many of the AMS tags;
+arc_parse_line() for only the AMS is called asking for all the tag types.
+That context is then discarded.
+
+Later, for Verify, we look at ARC headers again and then grab the hash result
+from the DKIM layer. arc_parse_line() is called for all 3 line types,
+gathering info for only 'i' and 'ip' tags from AAR headers,
+for all tag types from AMS and AS headers.
+
+
+For Sign, while running through the existing headers (before adding any for
+this signing operation, we "take copies" of the headers, we call
+arc_parse_line() gathering only the 'i' tag (instance) information.
+*/
/******************************************************************************/
}
DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i);
-*pas = as = store_get(sizeof(arc_set));
+*pas = as = store_get(sizeof(arc_set), GET_UNTAINTED);
memset(as, 0, sizeof(arc_set));
as->next = next;
as->prev = prev;
/* Inspect a header line, noting known tag fields.
-Check for duplicates. */
+Check for duplicate named tags.
+
+See the file block comment for how this is used.
+
+Return: NULL for good, or an error string
+*/
static uschar *
-arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only)
+arc_parse_line(arc_line * al, header_line * h, unsigned off, line_extract_t l_ext)
{
uschar * s = h->text + off;
-uschar * r = NULL; /* compiler-quietening */
+uschar * r = NULL;
uschar c;
al->complete = h;
-if (!instance_only)
+if (l_ext == le_all) /* need to grab rawsig_no_b */
{
- al->rawsig_no_b_val.data = store_get(h->slen + 1);
+ al->rawsig_no_b_val.data = store_get(h->slen + 1, GET_TAINTED);
memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */
r = al->rawsig_no_b_val.data + off;
al->rawsig_no_b_val.len = off;
uschar * bstart = NULL, * bend;
/* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */
+ /*X or just a naked FQDN, in a AAR ! */
- s = skip_fws(s); /* FWS */
+ s = skip_fws(s); /* leading FWS */
if (!*s) break;
-/* debug_printf("%s: consider '%s'\n", __FUNCTION__, s); */
tagchar = *s++;
- s = skip_fws(s); /* FWS */
- if (!*s) break;
+ if (!*(s = skip_fws(s))) break; /* FWS */
- if (!instance_only || tagchar == 'i') switch (tagchar)
+ switch (tagchar)
{
case 'a': /* a= AMS algorithm */
- {
- if (*s != '=') return US"no 'a' value";
- if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup";
-
- /* substructure: algo-hash (eg. rsa-sha256) */
-
- t = al->a_algo.data = al->a.data;
- while (*t != '-')
- if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value";
- al->a_algo.len = i;
- if (*t++ != '-') return US"no '-' in 'a' value";
- al->a_hash.data = t;
- al->a_hash.len = al->a.len - i - 1;
- }
+ if (l_ext == le_all && *s == '=')
+ {
+ if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup";
+
+ /* substructure: algo-hash (eg. rsa-sha256) */
+
+ t = al->a_algo.data = al->a.data;
+ while (*t != '-')
+ if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value";
+ al->a_algo.len = i;
+ if (*t++ != '-') return US"no '-' in 'a' value";
+ al->a_hash.data = t;
+ al->a_hash.len = al->a.len - i - 1;
+ }
break;
case 'b':
- {
- gstring * g = NULL;
-
- switch (*s)
+ if (l_ext == le_all)
{
- case '=': /* b= AMS signature */
- if (al->b.data) return US"already b data";
- bstart = s+1;
-
- /* The signature can have FWS inserted in the content;
- make a stripped copy */
-
- while ((c = *++s) && c != ';')
- if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
- g = string_catn(g, s, 1);
- al->b.data = string_from_gstring(g);
- al->b.len = g->ptr;
- gstring_reset_unused(g);
- bend = s;
- break;
- case 'h': /* bh= AMS body hash */
- s = skip_fws(++s); /* FWS */
- if (*s != '=') return US"no bh value";
- if (al->bh.data) return US"already bh data";
-
- /* The bodyhash can have FWS inserted in the content;
- make a stripped copy */
-
- while ((c = *++s) && c != ';')
- if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
- g = string_catn(g, s, 1);
- al->bh.data = string_from_gstring(g);
- al->bh.len = g->ptr;
- gstring_reset_unused(g);
- break;
- default:
- return US"b? tag";
+ gstring * g = NULL;
+
+ switch (*s)
+ {
+ case '=': /* b= AMS signature */
+ if (al->b.data) return US"already b data";
+ bstart = s+1;
+
+ /* The signature can have FWS inserted in the content;
+ make a stripped copy */
+
+ while ((c = *++s) && c != ';')
+ if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
+ g = string_catn(g, s, 1);
+ if (!g) return US"no b= value";
+ al->b.len = len_string_from_gstring(g, &al->b.data);
+ gstring_release_unused(g);
+ bend = s;
+ break;
+ case 'h': /* bh= AMS body hash */
+ s = skip_fws(++s); /* FWS */
+ if (*s == '=')
+ {
+ if (al->bh.data) return US"already bh data";
+
+ /* The bodyhash can have FWS inserted in the content;
+ make a stripped copy */
+
+ while ((c = *++s) && c != ';')
+ if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
+ g = string_catn(g, s, 1);
+ if (!g) return US"no bh= value";
+ al->bh.len = len_string_from_gstring(g, &al->bh.data);
+ gstring_release_unused(g);
+ }
+ break;
+ default:
+ return US"b? tag";
+ }
}
- }
break;
case 'c':
- switch (*s)
+ if (l_ext == le_all) switch (*s)
{
case '=': /* c= AMS canonicalisation */
if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup";
}
break;
case 'v': /* cv= AS validity */
- if (*++s != '=') return US"cv tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) return US"cv tag dup";
+ s = skip_fws(s);
+ if (*++s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s))
+ return US"cv tag dup";
break;
- default:
- return US"c? tag";
}
break;
case 'd': /* d= AMS domain */
- if (*s != '=') return US"d tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) return US"d tag dup";
+ if (l_ext == le_all && *s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s))
+ return US"d tag dup";
break;
case 'h': /* h= AMS headers */
- if (*s != '=') return US"h tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) return US"h tag dup";
+ if (*s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s))
+ return US"h tag dup";
break;
case 'i': /* i= ARC set instance */
- if (*s != '=') return US"i tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) return US"i tag dup";
- if (instance_only) goto done;
+ if (*s == '=')
+ {
+ if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s))
+ return US"i tag dup";
+ if (l_ext == le_instance_only)
+ goto done; /* early-out */
+ }
break;
case 'l': /* l= bodylength */
- if (*s != '=') return US"l tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) return US"l tag dup";
+ if (l_ext == le_all && *s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s))
+ return US"l tag dup";
break;
- case 's': /* s= AMS selector */
- if (*s != '=') return US"s tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) return US"s tag dup";
+ case 's':
+ if (*s == '=' && l_ext == le_all)
+ {
+ if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s))
+ return US"s tag dup";
+ }
+ else if ( l_ext == le_instance_plus_ip
+ && Ustrncmp(s, "mtp.remote-ip", 13) == 0)
+ { /* smtp.remote-ip= AAR reception data */
+ s += 13;
+ s = skip_fws(s);
+ if (*s != '=') return US"smtp.remote_ip tag val";
+ if (arc_insert_tagvalue(al, offsetof(arc_line, ip), &s))
+ return US"ip tag dup";
+ }
break;
}
- while ((c = *s) && c != ';') s++;
+ while ((c = *s) && c != ';') s++; /* end of this tag=value */
if (c) s++; /* ; after tag-spec */
/* for all but the b= tag, copy the field including FWS. For the b=,
drop the tag content. */
- if (!instance_only)
+ if (r)
if (bstart)
{
size_t n = bstart - fieldstart;
}
}
-if (!instance_only)
+if (r)
*r = '\0';
done:
static uschar *
arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
- BOOL instance_only)
+ line_extract_t l_ext, arc_line ** alp_ret)
{
-int i;
+unsigned i;
arc_set * as;
-arc_line * al = store_get(sizeof(arc_line)), ** alp;
+arc_line * al = store_get(sizeof(arc_line), GET_UNTAINTED), ** alp;
uschar * e;
memset(al, 0, sizeof(arc_line));
-if ((e = arc_parse_line(al, h, off, instance_only)))
+if ((e = arc_parse_line(al, h, off, l_ext)))
{
DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e);
- return US"line parse";
+ return string_sprintf("line parse: %s", e);
}
if (!(i = arc_instance_from_hdr(al))) return US"instance find";
+if (i > 50) return US"overlarge instance number";
if (!(as = arc_find_set(ctx, i))) return US"set find";
if (*(alp = (arc_line **)(US as + hoff))) return US"dup hdr";
*alp = al;
+if (alp_ret) *alp_ret = al;
return NULL;
}
+/* Called for both Sign and Verify */
static const uschar *
-arc_try_header(arc_ctx * ctx, header_line * h, BOOL instance_only)
+arc_try_header(arc_ctx * ctx, header_line * h, BOOL is_signing)
{
const uschar * e;
debug_printf("ARC: found AAR: %.*s\n", len, h->text);
}
if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar),
- TRUE)))
+ is_signing ? le_instance_only : le_instance_plus_ip, NULL)))
{
DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e);
- return US"inserting AAR";
+ return string_sprintf("inserting AAR: %s", e);
}
}
else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0)
debug_printf("ARC: found AMS: %.*s\n", len, h->text);
}
if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams),
- instance_only)))
+ is_signing ? le_instance_only : le_all, &ams)))
{
DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e);
- return US"inserting AMS";
+ return string_sprintf("inserting AMS: %s", e);
}
/* defaults */
- /*XXX dubious selection of ams here */
- ams = ctx->arcset_chain->hdr_ams;
if (!ams->c.data)
{
ams->c_head.data = US"simple"; ams->c_head.len = 6;
debug_printf("ARC: found AS: %.*s\n", len, h->text);
}
if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as),
- instance_only)))
+ is_signing ? le_instance_only : le_all, NULL)))
{
DEBUG(D_acl) debug_printf("inserting AS: %s\n", e);
- return US"inserting AS";
+ return string_sprintf("inserting AS: %s", e);
}
}
return NULL;
/* Gather the chain of arc sets from the headers.
Check for duplicates while that is done. Also build the
-reverse-order headers list;
+reverse-order headers list.
+Called on an ACL verify=arc condition.
Return: ARC state if determined, eg. by lack of any ARC chain.
*/
DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n");
for (h = header_list; h; h = h->next)
{
- r = store_get(sizeof(hdr_rlist));
+ r = store_get(sizeof(hdr_rlist), GET_UNTAINTED);
r->prev = rprev;
r->used = FALSE;
r->h = h;
const uschar * s;
int len;
-if (!exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
+if ( hashtype == -1
+ || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
{
DEBUG(D_acl)
debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
len = Ustrlen(s);
DEBUG(D_acl) pdkim_quoteprint(s, len);
- exim_sha_update(&hhash_ctx, s, Ustrlen(s));
+ exim_sha_update_string(&hhash_ctx, s);
r->used = TRUE;
break;
}
pdkim_pubkey * p;
if (!(dns_txt = dkim_exim_query_dns_txt(string_sprintf("%.*s._domainkey.%.*s",
- al->s.len, al->s.data, al->d.len, al->d.data))))
+ (int)al->s.len, al->s.data, (int)al->d.len, al->d.data))))
{
DEBUG(D_acl) debug_printf("pubkey dns lookup fail\n");
return NULL;
static pdkim_bodyhash *
arc_ams_setup_vfy_bodyhash(arc_line * ams)
{
-int canon_head, canon_body;
+int canon_head = -1, canon_body = -1;
long bodylen;
+if (!ams->c.data) ams->c.data = US"simple"; /* RFC 6376 (DKIM) default */
pdkim_cstring_to_canons(ams->c.data, ams->c.len, &canon_head, &canon_body);
bodylen = ams->l.data
? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) : -1;
/* Setup the interface to the signing library */
-if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx)))
+if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx, NULL)))
{
DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr);
as->ams_verify_done = arc_state_reason = US"internal sigverify init error";
}
hashtype = pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len);
+if (hashtype == -1)
+ {
+ DEBUG(D_acl) debug_printf("ARC i=%d AMS verify bad a_hash\n", as->instance);
+ return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify";
+ }
if ((errstr = exim_dkim_verify(&vctx,
pdkim_hashes[hashtype].exim_hashmethod, &hhash, &sighash)))
arc_set * as;
int inst;
BOOL ams_fail_found = FALSE;
-uschar * ret = NULL;
-if (!(as = ctx->arcset_chain))
+if (!(as = ctx->arcset_chain_last))
return US"none";
-for(inst = 0; as; as = as->next)
+for(inst = as->instance; as; as = as->prev, inst--)
{
- if ( as->instance != ++inst
- || !as->hdr_aar || !as->hdr_ams || !as->hdr_as
- || arc_cv_match(as->hdr_as, US"fail")
- )
- {
- arc_state_reason = string_sprintf("i=%d fail"
- " (cv, sequence or missing header)", as->instance);
- DEBUG(D_acl) debug_printf("ARC %s\n", arc_state_reason);
- ret = US"fail";
- }
+ if (as->instance != inst)
+ arc_state_reason = string_sprintf("i=%d (sequence; expected %d)",
+ as->instance, inst);
+ else if (!as->hdr_aar || !as->hdr_ams || !as->hdr_as)
+ arc_state_reason = string_sprintf("i=%d (missing header)", as->instance);
+ else if (arc_cv_match(as->hdr_as, US"fail"))
+ arc_state_reason = string_sprintf("i=%d (cv)", as->instance);
+ else
+ goto good;
+
+ DEBUG(D_acl) debug_printf("ARC chain fail at %s\n", arc_state_reason);
+ return US"fail";
+ good:
/* Evaluate the oldest-pass AMS validation while we're here.
It does not affect the AS chain validation but is reported as
auxilary info. */
arc_oldest_pass = inst;
arc_state_reason = NULL;
}
+if (inst != 0)
+ {
+ arc_state_reason = string_sprintf("(sequence; expected i=%d)", inst);
+ DEBUG(D_acl) debug_printf("ARC chain fail %s\n", arc_state_reason);
+ return US"fail";
+ }
arc_received = ctx->arcset_chain_last;
-arc_received_instance = inst;
-if (ret)
- return ret;
+arc_received_instance = arc_received->instance;
/* We can skip the latest-AMS validation, if we already did it. */
as = ctx->arcset_chain_last;
-if (as->ams_verify_done && !as->ams_verify_passed)
+if (!as->ams_verify_passed)
{
- arc_state_reason = as->ams_verify_done;
- return US"fail";
+ if (as->ams_verify_done)
+ {
+ arc_state_reason = as->ams_verify_done;
+ return US"fail";
+ }
+ if (!!arc_ams_verify(ctx, as))
+ return US"fail";
}
-if (!!arc_ams_verify(ctx, as))
- return US"fail";
-
return NULL;
}
hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len);
-if (!exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
+if ( hashtype == -1
+ || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
{
DEBUG(D_acl)
debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
/* We know the b-tag blob is of a nul-term string, so safe as a string */
pdkim_decode_base64(hdr_as->b.data, &sighash);
-if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx)))
+if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx, NULL)))
{
DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr);
return US"fail";
}
-hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len);
-
if ((errstr = exim_dkim_verify(&vctx,
pdkim_hashes[hashtype].exim_hashmethod,
&hhash_computed, &sighash)))
static const uschar *
arc_verify_seals(arc_ctx * ctx)
{
-arc_set * as = ctx->arcset_chain;
+arc_set * as = ctx->arcset_chain_last;
if (!as)
return US"none";
-while (as)
- {
- if (arc_seal_verify(ctx, as)) return US"fail";
- as = as->next;
- }
+for ( ; as; as = as->prev) if (arc_seal_verify(ctx, as)) return US"fail";
+
DEBUG(D_acl) debug_printf("ARC: AS vfy overall pass\n");
return NULL;
}
const uschar *
acl_verify_arc(void)
{
-arc_ctx ctx = { NULL };
const uschar * res;
+memset(&arc_verify_ctx, 0, sizeof(arc_verify_ctx));
+
+if (!dkim_verify_ctx)
+ {
+ DEBUG(D_acl) debug_printf("ARC: no DKIM verify context\n");
+ return NULL;
+ }
+
/* AS evaluation, per
https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6
*/
none, the ARC state is "none" and the algorithm stops here.
*/
-if ((res = arc_vfy_collect_hdrs(&ctx)))
+if ((res = arc_vfy_collect_hdrs(&arc_verify_ctx)))
goto out;
/* 2. If the form of any ARC set is invalid (e.g., does not contain
then the chain state is "fail" and the algorithm stops here.
*/
-if ((res = arc_headers_check(&ctx)))
+if ((res = arc_headers_check(&arc_verify_ctx)))
goto out;
/* 4. For each ARC-Seal from the "N"th instance to the first, apply the
the algorithm is complete.
*/
-if ((res = arc_verify_seals(&ctx)))
+if ((res = arc_verify_seals(&arc_verify_ctx)))
goto out;
res = US"pass";
static hdr_rlist *
arc_rlist_entry(hdr_rlist * list, const uschar * s, int len)
{
-hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line));
+hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line), GET_UNTAINTED);
header_line * h = r->h = (header_line *)(r+1);
r->prev = list;
h->slen = len;
h->text = US s;
-/* This works for either NL or CRLF lines; also nul-termination */
-while (*++s)
- if (*s == '\n' && s[1] != '\t' && s[1] != ' ') break;
-s++; /* move past end of line */
-
return r;
}
/* Walk the given headers strings identifying each header, and construct
-a reverse-order list. Also parse ARC-chain headers and build the chain
-struct, retaining pointers into the string.
+a reverse-order list.
*/
static hdr_rlist *
arc_sign_append_aar(gstring * g, arc_ctx * ctx,
const uschar * identity, int instance, blob * ar)
{
-int aar_off = g ? g->ptr : 0;
-arc_set * as = store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line));
+int aar_off = gstring_length(g);
+arc_set * as =
+ store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line), GET_UNTAINTED);
arc_line * al = (arc_line *)(as+1);
header_line * h = (header_line *)(al+1);
g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR);
-g = string_cat(g, string_sprintf(" i=%d; %s;\r\n\t", instance, identity));
+g = string_fmt_append(g, " i=%d; %s; smtp.remote-ip=%s;\r\n\t",
+ instance, identity, sender_host_address);
g = string_catn(g, US ar->data, ar->len);
h->slen = g->ptr - aar_off;
if ( (errstr = exim_dkim_signing_init(privkey, &sctx))
|| (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig)))
{
- log_write(0, LOG_MAIN|LOG_PANIC, "ARC: %s signing: %s\n", why, errstr);
+ log_write(0, LOG_MAIN, "ARC: %s signing: %s\n", why, errstr);
+ DEBUG(D_transport)
+ debug_printf("private key, or private-key file content, was: '%s'\n",
+ privkey);
return FALSE;
}
return TRUE;
g = string_catn(g, US"\r\n\t ", 5);
}
g = string_catn(g, US";\r\n", 3);
-gstring_reset_unused(g);
+gstring_release_unused(g);
string_from_gstring(g);
return g;
}
static gstring *
arc_sign_append_ams(gstring * g, arc_ctx * ctx, int instance,
const uschar * identity, const uschar * selector, blob * bodyhash,
- hdr_rlist * rheaders, const uschar * privkey)
+ hdr_rlist * rheaders, const uschar * privkey, unsigned options)
{
uschar * s;
gstring * hdata = NULL;
int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */
blob sig;
int ams_off;
-arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line));
+arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED);
header_line * h = (header_line *)(al+1);
/* debug_printf("%s\n", __FUNCTION__); */
/* Construct the to-be-signed AMS pseudo-header: everything but the sig. */
-ams_off = g->ptr;
-g = string_append(g, 10,
- ARC_HDR_AMS,
- US" i=", string_sprintf("%d", instance),
- US"; a=rsa-sha256; c=relaxed; d=", identity, /*XXX hardwired */
- US"; s=", selector,
- US";\r\n\tbh=", pdkim_encode_base64(bodyhash),
- US";\r\n\th=");
+ams_off = gstring_length(g);
+g = string_fmt_append(g, "%s i=%d; a=rsa-sha256; c=relaxed; d=%s; s=%s",
+ ARC_HDR_AMS, instance, identity, selector); /*XXX hardwired a= */
+if (options & ARC_SIGN_OPT_TSTAMP)
+ g = string_fmt_append(g, "; t=%lu", (u_long)now);
+if (options & ARC_SIGN_OPT_EXPIRE)
+ g = string_fmt_append(g, "; x=%lu", (u_long)expire);
+g = string_fmt_append(g, ";\r\n\tbh=%s;\r\n\th=",
+ pdkim_encode_base64(bodyhash));
for(col = 3; rheaders; rheaders = rheaders->prev)
{
/* Lose the last colon from the h= list */
-if (g->s[g->ptr - 1] == ':') g->ptr--;
+gstring_trim_trailing(g, ':');
g = string_catn(g, US";\r\n\tb=;", 7);
/* Lose the trailing semicolon from the psuedo-header, and append the signature
(folded over lines) and termination to complete it. */
-g->ptr--;
+gstring_trim(g, 1);
g = arc_sign_append_sig(g, &sig);
h->slen = g->ptr - ams_off;
static gstring *
arc_sign_prepend_as(gstring * arcset_interim, arc_ctx * ctx,
int instance, const uschar * identity, const uschar * selector, blob * ar,
- const uschar * privkey)
+ const uschar * privkey, unsigned options)
{
gstring * arcset;
-arc_set * as;
uschar * status = arc_ar_cv_status(ar);
-arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line));
+arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED);
header_line * h = (header_line *)(al+1);
+uschar * badline_str;
gstring * hdata = NULL;
int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */
- all ARC set headers, set-number order, aar then ams then as,
including self (but with an empty b= in self)
*/
+DEBUG(D_transport) debug_printf("ARC: building AS for status '%s'\n", status);
/* Construct the AS except for the signature */
-arcset = string_append(NULL, 10,
+arcset = string_append(NULL, 9,
ARC_HDR_AS,
US" i=", string_sprintf("%d", instance),
US"; cv=", status,
US"; a=rsa-sha256; d=", identity, /*XXX hardwired */
- US"; s=", selector, /*XXX same as AMS */
+ US"; s=", selector); /*XXX same as AMS */
+if (options & ARC_SIGN_OPT_TSTAMP)
+ arcset = string_append(arcset, 2,
+ US"; t=", string_sprintf("%lu", (u_long)now));
+arcset = string_cat(arcset,
US";\r\n\t b=;");
h->slen = arcset->ptr;
/* For any but "fail" chain-verify status, walk the entire chain in order by
instance. For fail, only the new arc-set. Accumulate the elements walked. */
-for (as = Ustrcmp(status, US"fail") == 0
+for (arc_set * as = Ustrcmp(status, US"fail") == 0
? ctx->arcset_chain_last : ctx->arcset_chain;
as; as = as->next)
{
+ arc_line * l;
/* Accumulate AAR then AMS then AS. Relaxed canonicalisation
is required per standard. */
- h = as->hdr_aar->complete;
+ badline_str = US"aar";
+ if (!(l = as->hdr_aar)) goto badline;
+ h = l->complete;
hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
- h = as->hdr_ams->complete;
+ badline_str = US"ams";
+ if (!(l = as->hdr_ams)) goto badline;
+ h = l->complete;
hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
- h = as->hdr_as->complete;
+ badline_str = US"as";
+ if (!(l = as->hdr_as)) goto badline;
+ h = l->complete;
hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, !!as->next));
}
/* Finally, append the AMS and AAR to the new AS */
return string_catn(arcset, arcset_interim->s, arcset_interim->ptr);
+
+badline:
+ DEBUG(D_transport)
+ debug_printf("ARC: while building AS, missing %s in chain\n", badline_str);
+ return NULL;
}
+void
+arc_sign_init(void)
+{
+memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx));
+headers_rlist = NULL;
+}
+
+
+
/* A "normal" header line, identified by DKIM processing. These arrive before
the call to arc_sign(), which carries any newly-created DKIM headers - and
those go textually before the normal ones in the message.
static const uschar *
arc_header_sign_feed(gstring * g)
{
-uschar * s = string_copyn(g->s, g->ptr);
+uschar * s = string_copy_from_gstring(g);
headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr);
return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE);
}
+/* Per RFCs 6376, 7489 the only allowed chars in either an ADMD id
+or a selector are ALPHA/DIGGIT/'-'/'.'
+
+Check, to help catch misconfigurations such as a missing selector
+element in the arc_sign list.
+*/
+
+static BOOL
+arc_valid_id(const uschar * s)
+{
+for (uschar c; c = *s++; )
+ if (!isalnum(c) && c != '-' && c != '.') return FALSE;
+return TRUE;
+}
+
+
+
/* ARC signing. Called from the smtp transport, if the arc_sign option is set.
The dkim_exim_sign() function has already been called, so will have hashed the
message body for us so long as we requested a hash previously.
Arguments:
- signspec Three-element colon-sep list: identity, selector, privkey
+ signspec Three-element colon-sep list: identity, selector, privkey.
+ Optional fourth element: comma-sep list of options.
Already expanded
sigheaders Any signature headers already generated, eg. by DKIM, or NULL
errstr Error string
gstring *
arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr)
{
-const uschar * identity, * selector, * privkey;
+const uschar * identity, * selector, * privkey, * opts, * s;
+unsigned options = 0;
int sep = 0;
header_line * headers;
hdr_rlist * rheaders;
gstring * g = NULL;
pdkim_bodyhash * b;
+expire = now = 0;
+
/* Parse the signing specification */
-identity = string_nextinlist(&signspec, &sep, NULL, 0);
-selector = string_nextinlist(&signspec, &sep, NULL, 0);
-if ( !*identity | !*selector
- || !(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey)
+if (!(identity = string_nextinlist(&signspec, &sep, NULL, 0)) || !*identity)
+ { s = US"identity"; goto bad_arg_ret; }
+if (!(selector = string_nextinlist(&signspec, &sep, NULL, 0)) || !*selector)
+ { s = US"selector"; goto bad_arg_ret; }
+if (!(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey)
+ { s = US"privkey"; goto bad_arg_ret; }
+if (!arc_valid_id(identity))
+ { s = US"identity"; goto bad_arg_ret; }
+if (!arc_valid_id(selector))
+ { s = US"selector"; goto bad_arg_ret; }
+if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey)))
+ goto ret_sigheaders;
+
+if ((opts = string_nextinlist(&signspec, &sep, NULL, 0)))
{
- log_write(0, LOG_MAIN|LOG_PANIC, "ARC: bad signing-specification (%s)",
- !*identity ? "identity" : !*selector ? "selector" : "private-key");
- return NULL;
+ int osep = ',';
+ while ((s = string_nextinlist(&opts, &osep, NULL, 0)))
+ if (Ustrcmp(s, "timestamps") == 0)
+ {
+ options |= ARC_SIGN_OPT_TSTAMP;
+ if (!now) now = time(NULL);
+ }
+ else if (Ustrncmp(s, "expire", 6) == 0)
+ {
+ options |= ARC_SIGN_OPT_EXPIRE;
+ if (*(s += 6) == '=')
+ if (*++s == '+')
+ {
+ if (!(expire = (time_t)atoi(CS ++s)))
+ expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA;
+ if (!now) now = time(NULL);
+ expire += now;
+ }
+ else
+ expire = (time_t)atol(CS s);
+ else
+ {
+ if (!now) now = time(NULL);
+ expire = now + ARC_SIGN_DEFAULT_EXPIRE_DELTA;
+ }
+ }
}
-if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey)))
- return NULL;
DEBUG(D_transport) debug_printf("ARC: sign for %s\n", identity);
-/*
-- scan headers for existing ARC chain & A-R (with matching system-identfier)
- - paniclog & skip on problems (no A-R)
-*/
-
-/* Make an rlist of any new DKIM headers, then add the "normals" rlist to it */
+/* Make an rlist of any new DKIM headers, then add the "normals" rlist to it.
+Then scan the list for an A-R header. */
string_from_gstring(sigheaders);
if ((rheaders = arc_sign_scan_headers(&arc_sign_ctx, sigheaders)))
{
hdr_rlist ** rp;
- for (rp = &rheaders; *rp; ) rp = &(*rp)->prev;
- *rp = headers_rlist;
- headers_rlist = rheaders;
+ for (rp = &headers_rlist; *rp; ) rp = &(*rp)->prev;
+ *rp = rheaders;
}
-else
- rheaders = headers_rlist;
+
/* Finally, build a normal-order headers list */
/*XXX only needed for hunt-the-AR? */
-{
-header_line * hnext = NULL;
-for (; rheaders; hnext = rheaders->h, rheaders = rheaders->prev)
- rheaders->h->next = hnext;
-headers = hnext;
-}
-
-instance = arc_sign_ctx.arcset_chain_last ? arc_sign_ctx.arcset_chain_last->instance + 1 : 1;
+/*XXX also, we really should be accepting any number of ADMD-matching ARs */
+ {
+ header_line * hnext = NULL;
+ for (rheaders = headers_rlist; rheaders;
+ hnext = rheaders->h, rheaders = rheaders->prev)
+ rheaders->h->next = hnext;
+ headers = hnext;
+ }
if (!(arc_sign_find_ar(headers, identity, &ar)))
{
- log_write(0, LOG_MAIN|LOG_PANIC, "ARC: no Authentication-Results header for signing");
- return sigheaders ? sigheaders : string_get(0);
+ log_write(0, LOG_MAIN, "ARC: no Authentication-Results header for signing");
+ goto ret_sigheaders;
}
+/* We previously built the data-struct for the existing ARC chain, if any, using a headers
+feed from the DKIM module. Use that to give the instance number for the ARC set we are
+about to build. */
+
+DEBUG(D_transport)
+ if (arc_sign_ctx.arcset_chain_last)
+ debug_printf("ARC: existing chain highest instance: %d\n",
+ arc_sign_ctx.arcset_chain_last->instance);
+ else
+ debug_printf("ARC: no existing chain\n");
+
+instance = arc_sign_ctx.arcset_chain_last ? arc_sign_ctx.arcset_chain_last->instance + 1 : 1;
+
/*
- Generate AAR
- copy the A-R; prepend i= & identity
b = arc_ams_setup_sign_bodyhash();
g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector,
- &b->bh, headers_rlist, privkey);
+ &b->bh, headers_rlist, privkey, options);
/*
- Generate AS
including self (but with an empty b= in self)
*/
-g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar, privkey);
+if (g)
+ g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar,
+ privkey, options);
/* Finally, append the dkim headers and return the lot. */
-g = string_catn(g, sigheaders->s, sigheaders->ptr);
-(void) string_from_gstring(g);
-gstring_reset_unused(g);
-return g;
+if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr);
+
+out:
+ if (!g) return string_get(1);
+ (void) string_from_gstring(g);
+ gstring_release_unused(g);
+ return g;
+
+
+bad_arg_ret:
+ log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)", s);
+ret_sigheaders:
+ g = sigheaders;
+ goto out;
}
DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n");
/* Parse the AMS header */
-h.next = NULL;
-h.slen = g->size;
-h.text = g->s;
memset(&al, 0, sizeof(arc_line));
-if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE)))
+h.next = NULL;
+h.slen = len_string_from_gstring(g, &h.text);
+if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, le_all)))
{
DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr);
- return US"line parsing error";
+ goto badline;
+ }
+
+if (!al.a_hash.data)
+ {
+ DEBUG(D_acl) debug_printf("ARC: no a_hash from '%.*s'\n", h.slen, h.text);
+ goto badline;
}
/* defaults */
should have been created here. */
return NULL;
+
+badline:
+ return US"line parsing error";
}
/******************************************************************************/
-/* Construct an Authenticate-Results header portion, for the ARC module */
+/* Construct the list of domains from the ARC chain after validation */
+
+uschar *
+fn_arc_domains(void)
+{
+arc_set * as;
+unsigned inst;
+gstring * g = NULL;
+
+for (as = arc_verify_ctx.arcset_chain, inst = 1; as; as = as->next, inst++)
+ {
+ arc_line * hdr_as = as->hdr_as;
+ if (hdr_as)
+ {
+ blob * d = &hdr_as->d;
+
+ for (; inst < as->instance; inst++)
+ g = string_catn(g, US":", 1);
+
+ g = d->data && d->len
+ ? string_append_listele_n(g, ':', d->data, d->len)
+ : string_catn(g, US":", 1);
+ }
+ else
+ g = string_catn(g, US":", 1);
+ }
+if (!g) return US"";
+return string_from_gstring(g);
+}
+
+
+/* Construct an Authentication-Results header portion, for the ARC module */
gstring *
authres_arc(gstring * g)
{
arc_line * highest_ams;
int start = 0; /* Compiler quietening */
- DEBUG(D_acl) start = g->ptr;
+ DEBUG(D_acl) start = gstring_length(g);
g = string_append(g, 2, US";\n\tarc=", arc_state);
if (arc_received_instance > 0)
{
- g = string_append(g, 3, US" (i=",
- string_sprintf("%d", arc_received_instance), US")");
+ g = string_fmt_append(g, " (i=%d)", arc_received_instance);
if (arc_state_reason)
g = string_append(g, 3, US"(", arc_state_reason, US")");
g = string_catn(g, US" header.s=", 10);
highest_ams = arc_received->hdr_ams;
g = string_catn(g, highest_ams->s.data, highest_ams->s.len);
- g = string_append(g, 2,
- US" arc.oldest-pass=", string_sprintf("%d", arc_oldest_pass));
+ g = string_fmt_append(g, " arc.oldest-pass=%d", arc_oldest_pass);
if (sender_host_address)
- g = string_append(g, 2, US" smtp.client-ip=", sender_host_address);
+ g = string_append(g, 2, US" smtp.remote-ip=", sender_host_address);
+ }
+ else if (arc_state_reason)
+ g = string_append(g, 3, US" (", arc_state_reason, US")");
+ DEBUG(D_acl) debug_printf("ARC:\tauthres '%.*s'\n",
+ gstring_length(g) - start - 3, g->s + start + 3);
+ }
+else
+ DEBUG(D_acl) debug_printf("ARC:\tno authres\n");
+return g;
+}
+
+
+# ifdef SUPPORT_DMARC
+/* Append a DMARC history record pair for ARC, to the given history set */
+
+gstring *
+arc_dmarc_hist_append(gstring * g)
+{
+if (arc_state)
+ {
+ BOOL first = TRUE;
+ int i = Ustrcmp(arc_state, "pass") == 0 ? ARES_RESULT_PASS
+ : Ustrcmp(arc_state, "fail") == 0 ? ARES_RESULT_FAIL
+ : ARES_RESULT_UNKNOWN;
+ g = string_fmt_append(g, "arc %d\n", i);
+ g = string_fmt_append(g, "arc_policy %d json[",
+ i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS
+ : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL
+ : DMARC_ARC_POLICY_RESULT_UNUSED);
+ /*XXX would we prefer this backwards? */
+ for (arc_set * as = arc_verify_ctx.arcset_chain; as;
+ as = as->next, first = FALSE)
+ {
+ arc_line * line = as->hdr_as;
+ if (line)
+ {
+ blob * d = &line->d;
+ blob * s = &line->s;
+
+ if (!first)
+ g = string_catn(g, US",", 1);
+
+ g = string_fmt_append(g, " (\"i\":%u," /*)*/
+ " \"d\":\"%.*s\","
+ " \"s\":\"%.*s\"",
+ as->instance,
+ d->data ? (int)d->len : 0, d->data && d->len ? d->data : US"",
+ s->data ? (int)s->len : 0, s->data && s->len ? s->data : US""
+ );
+ if ((line = as->hdr_aar))
+ {
+ blob * ip = &line->ip;
+ if (ip->data && ip->len)
+ g = string_fmt_append(g, ", \"ip\":\"%.*s\"", (int)ip->len, ip->data);
+ }
+
+ g = string_catn(g, US")", 1);
+ }
}
- DEBUG(D_acl) debug_printf("ARC: authres '%.*s'\n",
- g->ptr - start - 3, g->s + start + 3);
+ g = string_catn(g, US" ]\n", 3);
}
else
- DEBUG(D_acl) debug_printf("ARC: no authres\n");
+ g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
+ ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
return g;
}
+# endif
-# endif /* SUPPORT_SPF */
+# endif /* DISABLE_DKIM */
#endif /* EXPERIMENTAL_ARC */
/* vi: aw ai sw=2
*/