ARC: better diagnostics for keyfile issues
[exim.git] / src / src / arc.c
index 6f567dc5faf92bad38cd56d2ba48aabfc498fa75..dd2ad51b097dab098592290f02f20abe1f2a6145 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 {
@@ -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 };
 
 
 /******************************************************************************/
@@ -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";
 
@@ -761,22 +770,25 @@ arc_set * as;
 int inst;
 BOOL ams_fail_found = FALSE;
 
-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"
-      " (cv, sequence or missing header)", as->instance);
-    DEBUG(D_acl) debug_printf("ARC chain fail at %s\n", arc_state_reason);
-    return 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. */
@@ -788,9 +800,15 @@ for(inst = 0; as; as = as->next)
       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;
+arc_received_instance = arc_received->instance;
 
 /* We can skip the latest-AMS validation, if we already did it. */
 
@@ -990,9 +1008,10 @@ 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");
@@ -1006,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
@@ -1024,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
@@ -1066,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";
@@ -1239,6 +1258,9 @@ if (  (errstr = exim_dkim_signing_init(privkey, &sctx))
    || (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig)))
   {
   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;
@@ -1272,7 +1294,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;
@@ -1288,11 +1310,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=");
 
@@ -1390,7 +1419,7 @@ 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;
@@ -1417,12 +1446,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; d=", identity,                     /*XXX hardwired */
-         US"; s=", selector,                                   /*XXX same as AMS */
+         US"; s=", selector);                                  /*XXX same as AMS */
+if (options & ARC_SIGN_OPT_TSTAMP)
+  arcset = string_append(arcset, 2,
+      US"; t=", string_sprintf("%lu", (u_long)now));
+arcset = string_cat(arcset,
          US";\r\n\t b=;");
 
 h->slen = arcset->ptr;
@@ -1520,7 +1553,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
@@ -1533,7 +1567,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;
@@ -1542,11 +1577,13 @@ 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, "ARC: bad signing-specification (%s)",
@@ -1556,6 +1593,36 @@ if (  !*identity | !*selector
 if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey)))
   return sigheaders ? sigheaders : string_get(0);
 
+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;
+       }
+      }
+  }
+
 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.
@@ -1617,7 +1684,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
@@ -1632,7 +1699,9 @@ 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);
+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. */
 
@@ -1718,7 +1787,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)