* 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 - 2022
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
}
DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i);
-*pas = as = store_get(sizeof(arc_set), FALSE);
+*pas = as = store_get(sizeof(arc_set), GET_UNTAINTED);
memset(as, 0, sizeof(arc_set));
as->next = next;
as->prev = prev;
if (!instance_only)
{
- al->rawsig_no_b_val.data = store_get(h->slen + 1, TRUE); /* tainted */
+ 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;
if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
g = string_catn(g, s, 1);
if (!g) return US"no b= value";
- al->b.data = string_from_gstring(g);
- al->b.len = g->ptr;
+ al->b.len = len_string_from_gstring(g, &al->b.data);
gstring_release_unused(g);
bend = s;
break;
if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
g = string_catn(g, s, 1);
if (!g) return US"no bh= value";
- al->bh.data = string_from_gstring(g);
- al->bh.len = g->ptr;
+ al->bh.len = len_string_from_gstring(g, &al->bh.data);
gstring_release_unused(g);
break;
default:
static uschar *
arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
- BOOL instance_only)
+ BOOL instance_only, arc_line ** alp_ret)
{
unsigned i;
arc_set * as;
-arc_line * al = store_get(sizeof(arc_line), FALSE), ** alp;
+arc_line * al = store_get(sizeof(arc_line), GET_UNTAINTED), ** alp;
uschar * e;
memset(al, 0, sizeof(arc_line));
if (*(alp = (arc_line **)(US as + hoff))) return US"dup hdr";
*alp = al;
+if (alp_ret) *alp_ret = al;
return NULL;
}
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)))
+ TRUE, NULL)))
{
DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e);
return US"inserting AAR";
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)))
+ instance_only, &ams)))
{
DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e);
return US"inserting AMS";
}
/* 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)))
+ instance_only, NULL)))
{
DEBUG(D_acl) debug_printf("inserting AS: %s\n", e);
return US"inserting AS";
DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n");
for (h = header_list; h; h = h->next)
{
- r = store_get(sizeof(hdr_rlist), FALSE);
+ 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 */
/* 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)))
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 hdr_rlist *
arc_rlist_entry(hdr_rlist * list, const uschar * s, int len)
{
-hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line), FALSE);
+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;
}
arc_sign_append_aar(gstring * g, arc_ctx * ctx,
const uschar * identity, int instance, blob * ar)
{
-int aar_off = g ? g->ptr : 0;
+int aar_off = gstring_length(g);
arc_set * as =
- store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line), FALSE);
+ 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);
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), FALSE);
+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;
+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)
/* 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;
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), FALSE);
+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 */
/* 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;
}
arc_sign_init(void)
{
memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx));
+headers_rlist = NULL;
}
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.
/* 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)
- {
- log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)",
- !*identity ? "identity" : !*selector ? "selector" : "private-key");
- return sigheaders ? sigheaders : string_get(0);
- }
+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)))
- return sigheaders ? sigheaders : string_get(0);
+ goto ret_sigheaders;
if ((opts = string_nextinlist(&signspec, &sep, NULL, 0)))
{
if (!(arc_sign_find_ar(headers, identity, &ar)))
{
log_write(0, LOG_MAIN, "ARC: no Authentication-Results header for signing");
- return sigheaders ? sigheaders : string_get(0);
+ goto ret_sigheaders;
}
/* We previously built the data-struct for the existing ARC chain, if any, using a headers
/* Finally, append the dkim headers and return the lot. */
if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr);
-(void) string_from_gstring(g);
-gstring_release_unused(g);
-return g;
+
+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));
+h.next = NULL;
+h.slen = len_string_from_gstring(g, &h.text);
if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE)))
{
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";
}
else
g = string_catn(g, US":", 1);
}
-return g ? g->s : US"";
+if (!g) return US"";
+return string_from_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)
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);
+ gstring_length(g) - start - 3, g->s + start + 3);
}
else
DEBUG(D_acl) debug_printf("ARC: no authres\n");
}
-# endif /* SUPPORT_SPF */
+# endif /* DISABLE_DKIM */
#endif /* EXPERIMENTAL_ARC */
/* vi: aw ai sw=2
*/