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 {
#define HDR_AR US"Authentication-Results:"
#define HDRLEN_AR 23
+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 };
/******************************************************************************/
arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
BOOL instance_only)
{
-int i;
+unsigned i;
arc_set * as;
arc_line * al = store_get(sizeof(arc_line)), ** alp;
uschar * e;
return US"line parse";
}
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";
break;
}
-/* Finally add in the signature header (with the b= tag stripped) */
+/* Finally add in the signature header (with the b= tag stripped); no CRLF */
s = ams->rawsig_no_b_val.data, len = ams->rawsig_no_b_val.len;
if (relaxed)
- len = Ustrlen(s = pdkim_relax_header_n(s, len, TRUE));
+ len = Ustrlen(s = pdkim_relax_header_n(s, len, FALSE));
DEBUG(D_acl) pdkim_quoteprint(s, len);
exim_sha_update(&hhash_ctx, s, len);
int canon_head, canon_body;
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;
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;
}
header canonicalization defined in Section 3.4.2 of
[RFC6376]. Pass the canonicalized result to the hash
function.
+
+Headers are CRLF-separated, but the last one is not crlf-terminated.
*/
DEBUG(D_acl) debug_printf("ARC: AS header data for verification:\n");
al = as2->hdr_as;
if (as2->instance == as->instance)
s = pdkim_relax_header_n(al->rawsig_no_b_val.data,
- al->rawsig_no_b_val.len, TRUE);
+ al->rawsig_no_b_val.len, FALSE);
else if (!(s = al->relaxed))
al->relaxed = s = pdkim_relax_header_n(al->complete->text,
al->complete->slen, TRUE);
{
DEBUG(D_acl)
debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr);
- arc_state_reason = US"seal sigverify init error";
+ arc_state_reason = US"seal sigverify error";
return US"fail";
}
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";
/* 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 *
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;
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;
/* Construct the to-be-signed AMS pseudo-header: everything but the sig. */
ams_off = g->ptr;
-g = string_append(g, 10,
+g = string_append(g, 7,
ARC_HDR_AMS,
US" i=", string_sprintf("%d", instance),
US"; a=rsa-sha256; c=relaxed; d=", identity, /*XXX hardwired */
- US"; s=", selector,
+ US"; s=", selector);
+if (options & ARC_SIGN_OPT_TSTAMP)
+ g = string_append(g, 2,
+ US"; t=", string_sprintf("%lu", (u_long)now));
+if (options & ARC_SIGN_OPT_EXPIRE)
+ g = string_append(g, 2,
+ US"; x=", string_sprintf("%lu", (u_long)expire));
+g = string_append(g, 3,
US";\r\n\tbh=", pdkim_encode_base64(bodyhash),
US";\r\n\th=");
g = string_catn(g, US";\r\n\tb=;", 7);
/* Include the pseudo-header in the accumulation */
-/*XXX should that be prepended rather than appended? */
-/*XXX also need to include at the verify stage */
-s = pdkim_relax_header_n(g->s + ams_off, g->ptr - ams_off, TRUE);
+s = pdkim_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE);
hdata = string_cat(hdata, s);
/* Calculate the signature from the accumulation */
(c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++;
return string_copyn(methodspec, s - methodspec);
}
-return NULL;
+return US"none";
}
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;
/* 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; c=relaxed; d=", identity, /*XXX hardwired */
- US"; s=", selector, /*XXX same as AMS */
+ US"; a=rsa-sha256; d=", identity, /*XXX hardwired */
+ 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;
h = as->hdr_ams->complete;
hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
h = as->hdr_as->complete;
- hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
+ hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, !!as->next));
}
/* Calculate the signature from the accumulation */
+void
+arc_sign_init(void)
+{
+memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx));
+}
+
+
+
/* 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.
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
+if ( !*identity || !*selector
|| !(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey)
{
- log_write(0, LOG_MAIN|LOG_PANIC, "ARC: bad signing-specification");
- return NULL;
+ log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)",
+ !*identity ? "identity" : !*selector ? "selector" : "private-key");
+ return sigheaders ? sigheaders : string_get(0);
}
if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey)))
- return NULL;
+ return sigheaders ? sigheaders : string_get(0);
-DEBUG(D_transport) debug_printf("ARC: sign for %s\n", identity);
+if ((opts = string_nextinlist(&signspec, &sep, NULL, 0)))
+ {
+ 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(++s)))
+ expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA;
+ if (!now) now = time(NULL);
+ expire += now;
+ }
+ else
+ expire = (time_t)atol(s);
+ else
+ {
+ if (!now) now = time(NULL);
+ expire = now + ARC_SIGN_DEFAULT_EXPIRE_DELTA;
+ }
+ }
+ }
-/*
-- scan headers for existing ARC chain & A-R (with matching system-identfier)
- - paniclog & skip on problems (no A-R)
-*/
+DEBUG(D_transport) debug_printf("ARC: sign for %s\n", identity);
-/* 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");
+ log_write(0, LOG_MAIN, "ARC: no Authentication-Results header for signing");
return sigheaders ? sigheaders : string_get(0);
}
+/* 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);
+if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr);
(void) string_from_gstring(g);
gstring_reset_unused(g);
return g;
/******************************************************************************/
-/* 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);
+ }
+return g ? g->s : US"";
+}
+
+
+/* Construct an Authentication-Results header portion, for the ARC module */
gstring *
authres_arc(gstring * g)
if (sender_host_address)
g = string_append(g, 2, US" smtp.client-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: authres '%.*s'\n",
g->ptr - start - 3, g->s + start + 3);
}