ARC: add $arc_oldest_pass variable, for verify
[exim.git] / src / src / arc.c
index 1323a451a530079a8ce656997f06c66f0728ac6f..7374ba405b3bf805c8bf67770ede85ef388773b5 100644 (file)
 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 {
@@ -66,7 +71,7 @@ typedef struct arc_set {
   arc_line *           hdr_ams;
   arc_line *           hdr_as;
 
-  BOOL                 ams_verify_done;
+  const uschar *       ams_verify_done;
   BOOL                 ams_verify_passed;
 } arc_set;
 
@@ -84,8 +89,11 @@ typedef struct arc_ctx {
 #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 };
 
 
 /******************************************************************************/
@@ -96,9 +104,9 @@ Return 0 on error */
 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;
-return (unsigned) atoi(s);
+return (unsigned) atoi(CCS s);
 }
 
 
@@ -186,7 +194,7 @@ static uschar *
 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;
@@ -375,7 +383,7 @@ static uschar *
 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;
@@ -388,6 +396,7 @@ 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";
+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";
 
@@ -482,7 +491,7 @@ static const uschar *
 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");
@@ -495,7 +504,10 @@ for (h = header_list; h; h = h->next)
   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;
 
@@ -561,11 +573,11 @@ while ((hn = string_nextinlist(&headernames, &sep, NULL, 0)))
       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);
 
@@ -612,7 +624,7 @@ if (p->hashes)
   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;
     }
   }
@@ -628,6 +640,7 @@ arc_ams_setup_vfy_bodyhash(arc_line * ams)
 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;
@@ -644,7 +657,7 @@ return pdkim_set_bodyhash(dkim_verify_ctx,
 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;
@@ -656,7 +669,7 @@ ev_ctx vctx;
 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
@@ -668,21 +681,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)
+  {
+  as->ams_verify_done = arc_state_reason = US"required tag missing";
   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)))
+  {
+  as->ams_verify_done = arc_state_reason = US"internal hash setup error";
   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,
-              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);
   }
 
@@ -699,7 +718,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);
     }
-  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);
@@ -707,7 +726,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)))
-  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);
@@ -719,6 +738,7 @@ arc_get_verify_hhash(ctx, ams, &hhash);
 if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx)))
   {
   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";
   }
 
@@ -728,7 +748,7 @@ 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);
@@ -749,23 +769,26 @@ arc_headers_check(arc_ctx * ctx)
 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")
-     )
-    {
-    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. */
@@ -775,19 +798,31 @@ for(inst = 0; as; as = as->next)
       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_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 : !!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;
 }
 
@@ -822,7 +857,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
    )
+  {
+  arc_state_reason = US"seal cv state";
   return US"fail";
+  }
 
 /*
        3.  Initialize a hash function corresponding to the "a" tag of
@@ -835,6 +873,7 @@ if (!exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
   {
   DEBUG(D_acl)
       debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
+  arc_state_reason = US"seal hash setup error";
   return US"fail";
   }
 
@@ -844,6 +883,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.
+
+Headers are CRLF-separated, but the last one is not crlf-terminated.
 */
 
 DEBUG(D_acl) debug_printf("ARC: AS header data for verification:\n");
@@ -874,7 +915,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->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);
@@ -891,7 +932,7 @@ exim_sha_finish(&hhash_ctx, &hhash_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);
   }
 
@@ -931,6 +972,7 @@ if ((errstr = exim_dkim_verify(&vctx,
   {
   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";
   }
 
@@ -966,9 +1008,16 @@ Return:  The ARC state, or NULL on error.
 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
 */
@@ -976,7 +1025,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.
 */
 
-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
@@ -994,7 +1043,7 @@ if ((res = arc_vfy_collect_hdrs(&ctx)))
        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
@@ -1036,7 +1085,7 @@ if ((res = arc_headers_check(&ctx)))
        the algorithm is complete.
 */
 
-if ((res = arc_verify_seals(&ctx)))
+if ((res = arc_verify_seals(&arc_verify_ctx)))
   goto out;
 
 res = US"pass";
@@ -1072,8 +1121,7 @@ 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 *
@@ -1085,7 +1133,7 @@ hdr_rlist * rheaders = NULL;
 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)
@@ -1209,7 +1257,7 @@ else
 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);
   return FALSE;
   }
 return TRUE;
@@ -1243,7 +1291,7 @@ 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;
@@ -1259,11 +1307,18 @@ header_line * h = (header_line *)(al+1);
 /* 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=");
 
@@ -1306,10 +1361,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 */
-/*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 */
@@ -1353,7 +1406,7 @@ while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0)))
          (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++;
     return string_copyn(methodspec, s - methodspec);
     }
-return NULL;
+return US"none";
 }
 
 
@@ -1363,14 +1416,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,
-  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));
 header_line * h = (header_line *)(al+1);
-uschar * s;
 
 gstring * hdata = NULL;
 int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6);      /*XXX hardwired */
@@ -1391,12 +1443,16 @@ blob sig;
 
 /* 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;
@@ -1419,7 +1475,7 @@ for (as = Ustrcmp(status, US"fail") == 0
   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 */
@@ -1459,6 +1515,14 @@ return pdkim_set_bodyhash(&dkim_sign_ctx,
 
 
 
+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.
@@ -1486,7 +1550,8 @@ 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
@@ -1499,7 +1564,8 @@ Return value
 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;
@@ -1508,55 +1574,95 @@ int instance;
 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
@@ -1575,7 +1681,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->bh, headers_rlist, privkey);
+      &b->bh, headers_rlist, privkey, options);
 
 /*
 - Generate AS
@@ -1590,7 +1696,8 @@ g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector,
         including self (but with an empty b= in self)
 */
 
-g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar, privkey);
+g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar,
+      privkey, options);
 
 /* Finally, append the dkim headers and return the lot. */
 
@@ -1676,6 +1783,36 @@ return is_vfy ? arc_header_vfy_feed(g) : arc_header_sign_feed(g);
 
 /******************************************************************************/
 
+/* 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, ":", 1);
+
+    g = d->data && d->len
+      ? string_append_listele_n(g, ':', d->data, d->len)
+      : string_catn(g, ":", 1);
+    }
+  else
+    g = string_catn(g, ":", 1);
+  }
+return g ? g->s : US"";
+}
+
+
 /* Construct an Authenticate-Results header portion, for the ARC module */
 
 gstring *
@@ -1684,14 +1821,17 @@ authres_arc(gstring * g)
 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)
     {
     g = string_append(g, 3, US" (i=",
-      string_sprintf("%d", arc_received_instance), US") header.s=");
+      string_sprintf("%d", arc_received_instance), US")");
+    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);
 
@@ -1701,6 +1841,8 @@ if (arc_state)
     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);
   }