SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / arc.c
index 1323a451a530079a8ce656997f06c66f0728ac6f..e3a2b3dad723928f13884e830fa64ff8572cdac6 100644 (file)
@@ -2,15 +2,15 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 /* Experimental ARC support for Exim
 *     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
    License: GPL
+   SPDX-License-Identifier: GPL-2.0-only
 */
 
 #include "exim.h"
 */
 
 #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
 
 #  error DKIM must also be enabled for ARC
 # else
 
 extern pdkim_ctx * dkim_verify_ctx;
 extern pdkim_ctx dkim_sign_ctx;
 
 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 {
 /******************************************************************************/
 
 typedef struct hdr_rlist {
@@ -66,7 +71,7 @@ typedef struct arc_set {
   arc_line *           hdr_ams;
   arc_line *           hdr_as;
 
   arc_line *           hdr_ams;
   arc_line *           hdr_as;
 
-  BOOL                 ams_verify_done;
+  const uschar *       ams_verify_done;
   BOOL                 ams_verify_passed;
 } arc_set;
 
   BOOL                 ams_verify_passed;
 } arc_set;
 
@@ -84,8 +89,11 @@ typedef struct arc_ctx {
 #define HDR_AR         US"Authentication-Results:"
 #define HDRLEN_AR      23
 
 #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 hdr_rlist * headers_rlist;
 static arc_ctx arc_sign_ctx = { NULL };
+static arc_ctx arc_verify_ctx = { NULL };
 
 
 /******************************************************************************/
 
 
 /******************************************************************************/
@@ -96,9 +104,9 @@ Return 0 on error */
 static unsigned
 arc_instance_from_hdr(const arc_line * al)
 {
 static unsigned
 arc_instance_from_hdr(const arc_line * al)
 {
-uschar * s = al->i.data;
+const uschar * s = al->i.data;
 if (!s || !al->i.len) return 0;
 if (!s || !al->i.len) return 0;
-return (unsigned) atoi(s);
+return (unsigned) atoi(CCS s);
 }
 
 
 }
 
 
@@ -135,7 +143,7 @@ for (pas = &ctx->arcset_chain, prev = NULL, next = ctx->arcset_chain;
   }
 
 DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i);
   }
 
 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;
 memset(as, 0, sizeof(arc_set));
 as->next = next;
 as->prev = prev;
@@ -186,14 +194,14 @@ static uschar *
 arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only)
 {
 uschar * s = h->text + off;
 arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only)
 {
 uschar * s = h->text + off;
-uschar * r;
+uschar * r = NULL;     /* compiler-quietening */
 uschar c;
 
 al->complete = h;
 
 if (!instance_only)
   {
 uschar c;
 
 al->complete = h;
 
 if (!instance_only)
   {
-  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;
   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;
@@ -252,9 +260,10 @@ while ((c = *s))
          while ((c = *++s) && c != ';')
            if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
              g = string_catn(g, s, 1);
          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.data = string_from_gstring(g);
          al->b.len = g->ptr;
          al->b.data = string_from_gstring(g);
          al->b.len = g->ptr;
-         gstring_reset_unused(g);
+         gstring_release_unused(g);
          bend = s;
          break;
        case 'h':                       /* bh= AMS body hash */
          bend = s;
          break;
        case 'h':                       /* bh= AMS body hash */
@@ -268,9 +277,10 @@ while ((c = *s))
          while ((c = *++s) && c != ';')
            if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
              g = string_catn(g, s, 1);
          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.data = string_from_gstring(g);
          al->bh.len = g->ptr;
          al->bh.data = string_from_gstring(g);
          al->bh.len = g->ptr;
-         gstring_reset_unused(g);
+         gstring_release_unused(g);
          break;
        default:
          return US"b? tag";
          break;
        default:
          return US"b? tag";
@@ -373,11 +383,11 @@ adding instances as needed and checking for duplicate lines.
 
 static uschar *
 arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
 
 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)
 {
 {
-int i;
+unsigned i;
 arc_set * as;
 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));
 uschar * e;
 
 memset(al, 0, sizeof(arc_line));
@@ -388,10 +398,12 @@ if ((e = arc_parse_line(al, h, off, instance_only)))
   return US"line parse";
   }
 if (!(i = arc_instance_from_hdr(al)))  return US"instance find";
   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";
 
 *alp = al;
 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;
 }
 
 return NULL;
 }
 
@@ -415,7 +427,7 @@ if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0)
     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),
     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(D_acl) debug_printf("inserting AAR: %s\n", e);
     return US"inserting AAR";
@@ -434,15 +446,13 @@ 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),
     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 */
     {
     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;
   if (!ams->c.data)
     {
     ams->c_head.data = US"simple"; ams->c_head.len = 6;
@@ -460,7 +470,7 @@ else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0)
     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),
     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("inserting AS: %s\n", e);
     return US"inserting AS";
@@ -482,20 +492,23 @@ static const uschar *
 arc_vfy_collect_hdrs(arc_ctx * ctx)
 {
 header_line * h;
 arc_vfy_collect_hdrs(arc_ctx * ctx)
 {
 header_line * h;
-hdr_rlist * r, * rprev = NULL;
+hdr_rlist * r = NULL, * rprev = NULL;
 const uschar * e;
 
 DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n");
 for (h = header_list; h; h = h->next)
   {
 const uschar * e;
 
 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;
   rprev = r;
 
   if ((e = arc_try_header(ctx, h, FALSE)))
   r->prev = rprev;
   r->used = FALSE;
   r->h = h;
   rprev = r;
 
   if ((e = arc_try_header(ctx, h, FALSE)))
-    return e;
+    {
+    arc_state_reason = string_sprintf("collecting headers: %s", e);
+    return US"fail";
+    }
   }
 headers_rlist = r;
 
   }
 headers_rlist = r;
 
@@ -530,7 +543,8 @@ hctx hhash_ctx;
 const uschar * s;
 int len;
 
 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");
   {
   DEBUG(D_acl)
       debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
@@ -556,16 +570,16 @@ while ((hn = string_nextinlist(&headernames, &sep, NULL, 0)))
 
       len = Ustrlen(s);
       DEBUG(D_acl) pdkim_quoteprint(s, len);
 
       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;
       }
 
       r->used = TRUE;
       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)
 
 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);
 
 DEBUG(D_acl) pdkim_quoteprint(s, len);
 exim_sha_update(&hhash_ctx, s, len);
 
@@ -585,7 +599,7 @@ uschar * dns_txt;
 pdkim_pubkey * p;
 
 if (!(dns_txt = dkim_exim_query_dns_txt(string_sprintf("%.*s._domainkey.%.*s",
 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;
   {
   DEBUG(D_acl) debug_printf("pubkey dns lookup fail\n");
   return NULL;
@@ -612,7 +626,7 @@ if (p->hashes)
   if (!ele)
     {
     DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n",
   if (!ele)
     {
     DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n",
-                             p->hashes, al->a.len, al->a.data);
+                             p->hashes, (int)al->a.len, al->a.data);
     return NULL;
     }
   }
     return NULL;
     }
   }
@@ -625,9 +639,10 @@ return p;
 static pdkim_bodyhash *
 arc_ams_setup_vfy_bodyhash(arc_line * ams)
 {
 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;
 
 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;
 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;
@@ -644,7 +659,7 @@ return pdkim_set_bodyhash(dkim_verify_ctx,
 and without a DKIM v= tag.
 */
 
 and without a DKIM v= tag.
 */
 
-static uschar *
+static const uschar *
 arc_ams_verify(arc_ctx * ctx, arc_set * as)
 {
 arc_line * ams = as->hdr_ams;
 arc_ams_verify(arc_ctx * ctx, arc_set * as)
 {
 arc_line * ams = as->hdr_ams;
@@ -656,7 +671,7 @@ ev_ctx vctx;
 int hashtype;
 const uschar * errstr;
 
 int hashtype;
 const uschar * errstr;
 
-as->ams_verify_done = TRUE;
+as->ams_verify_done = US"in-progress";
 
 /* Check the AMS has all the required tags:
    "a="  algorithm
 
 /* Check the AMS has all the required tags:
    "a="  algorithm
@@ -668,21 +683,27 @@ as->ams_verify_done = TRUE;
 */
 if (  !ams->a.data || !ams->b.data || !ams->bh.data || !ams->d.data
    || !ams->h.data || !ams->s.data)
 */
 if (  !ams->a.data || !ams->b.data || !ams->bh.data || !ams->d.data
    || !ams->h.data || !ams->s.data)
+  {
+  as->ams_verify_done = arc_state_reason = US"required tag missing";
   return US"fail";
   return US"fail";
+  }
 
 
 /* The bodyhash should have been created earlier, and the dkim code should
 have managed calculating it during message input.  Find the reference to it. */
 
 if (!(b = arc_ams_setup_vfy_bodyhash(ams)))
 
 
 /* The bodyhash should have been created earlier, and the dkim code should
 have managed calculating it during message input.  Find the reference to it. */
 
 if (!(b = arc_ams_setup_vfy_bodyhash(ams)))
+  {
+  as->ams_verify_done = arc_state_reason = US"internal hash setup error";
   return US"fail";
   return US"fail";
+  }
 
 DEBUG(D_acl)
   {
   debug_printf("ARC i=%d AMS   Body bytes hashed: %lu\n"
               "              Body %.*s computed: ",
               as->instance, b->signed_body_bytes,
 
 DEBUG(D_acl)
   {
   debug_printf("ARC i=%d AMS   Body bytes hashed: %lu\n"
               "              Body %.*s computed: ",
               as->instance, b->signed_body_bytes,
-              ams->a_hash.len, ams->a_hash.data);
+              (int)ams->a_hash.len, ams->a_hash.data);
   pdkim_hexprint(CUS b->bh.data, b->bh.len);
   }
 
   pdkim_hexprint(CUS b->bh.data, b->bh.len);
   }
 
@@ -699,7 +720,7 @@ if (  !ams->bh.data
     pdkim_hexprint(sighash.data, sighash.len);
     debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance);
     }
     pdkim_hexprint(sighash.data, sighash.len);
     debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance);
     }
-  return US"body hash compare mismatch";
+  return as->ams_verify_done = arc_state_reason = US"AMS body hash miscompare";
   }
 
 DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance);
   }
 
 DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance);
@@ -707,7 +728,7 @@ DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance);
 /* Get the public key from DNS */
 
 if (!(p = arc_line_to_pubkey(ams)))
 /* Get the public key from DNS */
 
 if (!(p = arc_line_to_pubkey(ams)))
-  return US"pubkey problem";
+  return as->ams_verify_done = arc_state_reason = US"pubkey problem";
 
 /* We know the b-tag blob is of a nul-term string, so safe as a string */
 pdkim_decode_base64(ams->b.data, &sighash);
 
 /* We know the b-tag blob is of a nul-term string, so safe as a string */
 pdkim_decode_base64(ams->b.data, &sighash);
@@ -716,19 +737,25 @@ arc_get_verify_hhash(ctx, ams, &hhash);
 
 /* Setup the interface to the signing library */
 
 
 /* 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);
   {
   DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr);
+  as->ams_verify_done = arc_state_reason = US"internal sigverify init error";
   return US"fail";
   }
 
 hashtype = pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len);
   return US"fail";
   }
 
 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)))
   {
   DEBUG(D_acl) debug_printf("ARC i=%d AMS verify %s\n", as->instance, errstr);
 
 if ((errstr = exim_dkim_verify(&vctx,
          pdkim_hashes[hashtype].exim_hashmethod, &hhash, &sighash)))
   {
   DEBUG(D_acl) debug_printf("ARC i=%d AMS verify %s\n", as->instance, errstr);
-  return US"ams sig verify fail";
+  return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify";
   }
 
 DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance);
   }
 
 DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance);
@@ -749,23 +776,26 @@ arc_headers_check(arc_ctx * ctx)
 arc_set * as;
 int inst;
 BOOL ams_fail_found = FALSE;
 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";
 
   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")
-     )
-    {
-    DEBUG(D_acl) debug_printf("ARC i=%d fail"
-      " (cv, sequence or missing header)\n", as->instance);
-    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. */
   /* Evaluate the oldest-pass AMS validation while we're here.
   It does not affect the AS chain validation but is reported as
   auxilary info. */
@@ -775,19 +805,31 @@ for(inst = 0; as; as = as->next)
       ams_fail_found = TRUE;
     else
       arc_oldest_pass = inst;
       ams_fail_found = TRUE;
     else
       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 = 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;
 
 /* 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 : !!arc_ams_verify(ctx, as))
-  return US"fail";
-
+if (!as->ams_verify_passed)
+  {
+  if (as->ams_verify_done)
+    {
+    arc_state_reason = as->ams_verify_done;
+    return US"fail";
+    }
+  if (!!arc_ams_verify(ctx, as))
+    return US"fail";
+  }
 return NULL;
 }
 
 return NULL;
 }
 
@@ -822,7 +864,10 @@ DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance);
 if (  as->instance == 1 && !arc_cv_match(hdr_as, US"none")
    || arc_cv_match(hdr_as, US"none") && as->instance != 1
    )
 if (  as->instance == 1 && !arc_cv_match(hdr_as, US"none")
    || arc_cv_match(hdr_as, US"none") && as->instance != 1
    )
+  {
+  arc_state_reason = US"seal cv state";
   return US"fail";
   return US"fail";
+  }
 
 /*
        3.  Initialize a hash function corresponding to the "a" tag of
 
 /*
        3.  Initialize a hash function corresponding to the "a" tag of
@@ -831,10 +876,12 @@ if (  as->instance == 1 && !arc_cv_match(hdr_as, US"none")
 
 hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len);
 
 
 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");
   {
   DEBUG(D_acl)
       debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
+  arc_state_reason = US"seal hash setup error";
   return US"fail";
   }
 
   return US"fail";
   }
 
@@ -844,6 +891,8 @@ if (!exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
            header canonicalization defined in Section 3.4.2 of
            [RFC6376].  Pass the canonicalized result to the hash
            function.
            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");
 */
 
 DEBUG(D_acl) debug_printf("ARC: AS header data for verification:\n");
@@ -874,7 +923,7 @@ for (as2 = ctx->arcset_chain;
   al = as2->hdr_as;
   if (as2->instance == as->instance)
     s = pdkim_relax_header_n(al->rawsig_no_b_val.data,
   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);
   else if (!(s = al->relaxed))
     al->relaxed = s = pdkim_relax_header_n(al->complete->text,
                                            al->complete->slen, TRUE);
@@ -891,7 +940,7 @@ exim_sha_finish(&hhash_ctx, &hhash_computed);
 DEBUG(D_acl)
   {
   debug_printf("ARC i=%d AS Header %.*s computed: ",
 DEBUG(D_acl)
   {
   debug_printf("ARC i=%d AS Header %.*s computed: ",
-    as->instance, hdr_as->a_hash.len, hdr_as->a_hash.data);
+    as->instance, (int)hdr_as->a_hash.len, hdr_as->a_hash.data);
   pdkim_hexprint(hhash_computed.data, hhash_computed.len);
   }
 
   pdkim_hexprint(hhash_computed.data, hhash_computed.len);
   }
 
@@ -917,20 +966,19 @@ if (!(p = arc_line_to_pubkey(hdr_as)))
 /* 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);
 
 /* 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";
   }
 
   {
   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)))
   {
   DEBUG(D_acl)
     debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr);
 if ((errstr = exim_dkim_verify(&vctx,
              pdkim_hashes[hashtype].exim_hashmethod,
              &hhash_computed, &sighash)))
   {
   DEBUG(D_acl)
     debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr);
+  arc_state_reason = US"seal sigverify error";
   return US"fail";
   }
 
   return US"fail";
   }
 
@@ -942,16 +990,13 @@ return NULL;
 static const uschar *
 arc_verify_seals(arc_ctx * ctx)
 {
 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";
 
 
 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;
 }
 DEBUG(D_acl) debug_printf("ARC: AS vfy overall pass\n");
 return NULL;
 }
@@ -966,9 +1011,16 @@ Return:  The ARC state, or NULL on error.
 const uschar *
 acl_verify_arc(void)
 {
 const uschar *
 acl_verify_arc(void)
 {
-arc_ctx ctx = { NULL };
 const uschar * res;
 
 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
 */
 /* AS evaluation, per
 https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6
 */
@@ -976,7 +1028,7 @@ https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6
        none, the ARC state is "none" and the algorithm stops here.
 */
 
        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
   goto out;
 
 /* 2.  If the form of any ARC set is invalid (e.g., does not contain
@@ -994,7 +1046,7 @@ if ((res = arc_vfy_collect_hdrs(&ctx)))
        then the chain state is "fail" and the algorithm stops here.
 */
 
        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
   goto out;
 
 /* 4.  For each ARC-Seal from the "N"th instance to the first, apply the
@@ -1036,7 +1088,7 @@ if ((res = arc_headers_check(&ctx)))
        the algorithm is complete.
 */
 
        the algorithm is complete.
 */
 
-if ((res = arc_verify_seals(&ctx)))
+if ((res = arc_verify_seals(&arc_verify_ctx)))
   goto out;
 
 res = US"pass";
   goto out;
 
 res = US"pass";
@@ -1052,7 +1104,7 @@ out:
 static hdr_rlist *
 arc_rlist_entry(hdr_rlist * list, const uschar * s, int len)
 {
 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;
 header_line * h = r->h = (header_line *)(r+1);
 
 r->prev = list;
@@ -1062,18 +1114,12 @@ h->type = 0;
 h->slen = len;
 h->text = US s;
 
 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
 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 *
 */
 
 static hdr_rlist *
@@ -1085,7 +1131,7 @@ hdr_rlist * rheaders = NULL;
 s = sigheaders ? sigheaders->s : NULL;
 if (s) while (*s)
   {
 s = sigheaders ? sigheaders->s : NULL;
 if (s) while (*s)
   {
-  uschar * s2;
+  const uschar * s2 = s;
 
   /* This works for either NL or CRLF lines; also nul-termination */
   while (*++s2)
 
   /* This works for either NL or CRLF lines; also nul-termination */
   while (*++s2)
@@ -1143,13 +1189,14 @@ static gstring *
 arc_sign_append_aar(gstring * g, arc_ctx * ctx,
   const uschar * identity, int instance, blob * ar)
 {
 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);
 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;\r\n\t", instance, identity);
 g = string_catn(g, US ar->data, ar->len);
 
 h->slen = g->ptr - aar_off;
 g = string_catn(g, US ar->data, ar->len);
 
 h->slen = g->ptr - aar_off;
@@ -1209,7 +1256,10 @@ else
 if (  (errstr = exim_dkim_signing_init(privkey, &sctx))
    || (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig)))
   {
 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;
   return FALSE;
   }
 return TRUE;
@@ -1232,7 +1282,7 @@ for (;;)
   g = string_catn(g, US"\r\n\t  ", 5);
   }
 g = string_catn(g, US";\r\n", 3);
   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;
 }
 string_from_gstring(g);
 return g;
 }
@@ -1243,7 +1293,7 @@ return g;
 static gstring *
 arc_sign_append_ams(gstring * g, arc_ctx * ctx, int instance,
   const uschar * identity, const uschar * selector, blob * bodyhash,
 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;
 {
 uschar * s;
 gstring * hdata = NULL;
@@ -1251,7 +1301,7 @@ int col;
 int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6);      /*XXX hardwired */
 blob sig;
 int ams_off;
 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__); */
 header_line * h = (header_line *)(al+1);
 
 /* debug_printf("%s\n", __FUNCTION__); */
@@ -1259,13 +1309,14 @@ header_line * h = (header_line *)(al+1);
 /* Construct the to-be-signed AMS pseudo-header: everything but the sig. */
 
 ams_off = g->ptr;
 /* 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=");
+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)
   {
 
 for(col = 3; rheaders; rheaders = rheaders->prev)
   {
@@ -1306,10 +1357,8 @@ if (g->s[g->ptr - 1] == ':') g->ptr--;
 g = string_catn(g, US";\r\n\tb=;", 7);
 
 /* Include the pseudo-header in the accumulation */
 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 */
 hdata = string_cat(hdata, s);
 
 /* Calculate the signature from the accumulation */
@@ -1353,7 +1402,7 @@ while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0)))
          (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++;
     return string_copyn(methodspec, s - methodspec);
     }
          (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++;
     return string_copyn(methodspec, s - methodspec);
     }
-return NULL;
+return US"none";
 }
 
 
 }
 
 
@@ -1363,14 +1412,13 @@ return NULL;
 static gstring *
 arc_sign_prepend_as(gstring * arcset_interim, arc_ctx * ctx,
   int instance, const uschar * identity, const uschar * selector, blob * ar,
 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;
 {
 gstring * arcset;
-arc_set * as;
 uschar * status = arc_ar_cv_status(ar);
 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);
 header_line * h = (header_line *)(al+1);
-uschar * s;
+uschar * badline_str;
 
 gstring * hdata = NULL;
 int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6);      /*XXX hardwired */
 
 gstring * hdata = NULL;
 int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6);      /*XXX hardwired */
@@ -1388,15 +1436,20 @@ blob sig;
       - all ARC set headers, set-number order, aar then ams then as,
         including self (but with an empty b= in self)
 */
       - 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 */
 
 
 /* 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,
          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;
          US";\r\n\t b=;");
 
 h->slen = arcset->ptr;
@@ -1407,19 +1460,26 @@ ctx->arcset_chain_last->hdr_as = al;
 /* 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 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)
   {
        ? 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. */
 
   /* Accumulate AAR then AMS then AS.  Relaxed canonicalisation
   is required per standard. */
 
-  h = as->hdr_aar->complete;
-  hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
-  h = as->hdr_ams->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));
   hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
-  h = as->hdr_as->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));
   hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
+  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));
   }
 
 /* Calculate the signature from the accumulation */
   }
 
 /* Calculate the signature from the accumulation */
@@ -1435,6 +1495,11 @@ DEBUG(D_transport) debug_printf("ARC: AS  '%.*s'\n", arcset->ptr - 2, arcset->s)
 /* Finally, append the AMS and AAR to the new AS */
 
 return string_catn(arcset, arcset_interim->s, arcset_interim->ptr);
 /* 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;
 }
 
 
 }
 
 
@@ -1459,6 +1524,15 @@ return pdkim_set_bodyhash(&dkim_sign_ctx,
 
 
 
 
 
 
+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.
 /* 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.
@@ -1481,12 +1555,30 @@ 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:
 /* 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
                Already expanded
   sigheaders   Any signature headers already generated, eg. by DKIM, or NULL
   errstr       Error string
@@ -1499,7 +1591,8 @@ Return value
 gstring *
 arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr)
 {
 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;
 int sep = 0;
 header_line * headers;
 hdr_rlist * rheaders;
@@ -1508,55 +1601,96 @@ int instance;
 gstring * g = NULL;
 pdkim_bodyhash * b;
 
 gstring * g = NULL;
 pdkim_bodyhash * b;
 
+expire = now = 0;
+
 /* Parse the signing specification */
 
 /* 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");
-  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);
 
 
 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;
 
 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? */
 /* 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)))
   {
 
 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
 /*
 - Generate AAR
   - copy the A-R; prepend i= & identity
@@ -1575,7 +1709,7 @@ g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar);
 
 b = arc_ams_setup_sign_bodyhash();
 g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector,
 
 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
 
 /*
 - Generate AS
@@ -1590,14 +1724,26 @@ g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector,
         including self (but with an empty b= in self)
 */
 
         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. */
 
 
 /* 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;
 }
 
 
 }
 
 
@@ -1633,7 +1779,13 @@ memset(&al, 0, sizeof(arc_line));
 if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE)))
   {
   DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr);
 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 */
   }
 
 /* defaults */
@@ -1652,6 +1804,9 @@ if (!(b = arc_ams_setup_vfy_bodyhash(&al)))
 should have been created here. */
 
 return NULL;
 should have been created here. */
 
 return NULL;
+
+badline:
+  return US"line parsing error";
 }
 
 
 }
 
 
@@ -1676,7 +1831,37 @@ return is_vfy ? arc_header_vfy_feed(g) : arc_header_sign_feed(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)
 
 gstring *
 authres_arc(gstring * g)
@@ -1684,23 +1869,26 @@ authres_arc(gstring * g)
 if (arc_state)
   {
   arc_line * highest_ams;
 if (arc_state)
   {
   arc_line * highest_ams;
-  int start;
+  int start = 0;               /* Compiler quietening */
   DEBUG(D_acl) start = g->ptr;
 
   g = string_append(g, 2, US";\n\tarc=", arc_state);
   if (arc_received_instance > 0)
     {
   DEBUG(D_acl) start = g->ptr;
 
   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") header.s=");
+    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);
 
     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)
 
     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:  authres '%.*s'\n",
                  g->ptr - start - 3, g->s + start + 3);
   }
   DEBUG(D_acl) debug_printf("ARC:  authres '%.*s'\n",
                  g->ptr - start - 3, g->s + start + 3);
   }
@@ -1710,7 +1898,7 @@ return g;
 }
 
 
 }
 
 
-# endif /* SUPPORT_SPF */
+# endif /* DISABLE_DKIM */
 #endif /* EXPERIMENTAL_ARC */
 /* vi: aw ai sw=2
  */
 #endif /* EXPERIMENTAL_ARC */
 /* vi: aw ai sw=2
  */