ARC: add $arc_oldest_pass variable, for verify
[exim.git] / src / src / arc.c
index dedf64c444341ed708ce2d907ff1d8d2384c298a..7374ba405b3bf805c8bf67770ede85ef388773b5 100644 (file)
 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 {
@@ -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 };
 
 
 /******************************************************************************/
 
 
 /******************************************************************************/
@@ -375,7 +383,7 @@ static uschar *
 arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
   BOOL instance_only)
 {
 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;
 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";
   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";
 
 if (!(as = arc_find_set(ctx, i)))      return US"set find";
 if (*(alp = (arc_line **)(US as + hoff))) return US"dup hdr";
 
@@ -760,24 +769,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")
-     )
-    {
-    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. */
   /* Evaluate the oldest-pass AMS validation while we're here.
   It does not affect the AS chain validation but is reported as
   auxilary info. */
@@ -789,23 +800,29 @@ for(inst = 0; as; as = as->next)
       arc_oldest_pass = inst;
   arc_state_reason = NULL;
   }
       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)
+if (!as->ams_verify_passed)
   {
   {
-  arc_state_reason = as->ams_verify_done;
-  return US"fail";
+  if (as->ams_verify_done)
+    {
+    arc_state_reason = as->ams_verify_done;
+    return US"fail";
+    }
+  if (!!arc_ams_verify(ctx, as))
+    return US"fail";
   }
   }
-if (!!arc_ams_verify(ctx, as))
-  return US"fail";
-
 return NULL;
 }
 
 return NULL;
 }
 
@@ -991,9 +1008,10 @@ 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");
 if (!dkim_verify_ctx)
   {
   DEBUG(D_acl) debug_printf("ARC: no DKIM verify context\n");
@@ -1007,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.
 */
 
        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
@@ -1025,7 +1043,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
@@ -1067,7 +1085,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";
@@ -1273,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,
 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;
@@ -1289,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;
 /* 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 */
       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=");
 
       US";\r\n\tbh=", pdkim_encode_base64(bodyhash),
       US";\r\n\th=");
 
@@ -1391,7 +1416,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,
 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;
 {
 gstring * arcset;
 arc_set * as;
@@ -1418,12 +1443,16 @@ blob sig;
 
 /* 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,
          US"; a=rsa-sha256; d=", identity,                     /*XXX hardwired */
          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;
          US";\r\n\t b=;");
 
 h->slen = arcset->ptr;
@@ -1521,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:
 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
@@ -1534,7 +1564,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;
@@ -1543,11 +1574,13 @@ int instance;
 gstring * g = NULL;
 pdkim_bodyhash * b;
 
 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);
 /* 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)",
    || !(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey)
   {
   log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)",
@@ -1557,6 +1590,36 @@ if (  !*identity | !*selector
 if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey)))
   return sigheaders ? sigheaders : string_get(0);
 
 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.
 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.
@@ -1566,18 +1629,17 @@ string_from_gstring(sigheaders);
 if ((rheaders = arc_sign_scan_headers(&arc_sign_ctx, sigheaders)))
   {
   hdr_rlist ** rp;
 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? */
+/*XXX also, we really should be accepting any number of ADMD-matching ARs */
   {
   header_line * hnext = NULL;
   {
   header_line * hnext = NULL;
-  for (; rheaders; hnext = rheaders->h, rheaders = rheaders->prev)
+  for (rheaders = headers_rlist; rheaders;
+       hnext = rheaders->h, rheaders = rheaders->prev)
     rheaders->h->next = hnext;
   headers = hnext;
   }
     rheaders->h->next = hnext;
   headers = hnext;
   }
@@ -1619,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 = 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
@@ -1634,7 +1696,8 @@ 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);
+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. */
 
@@ -1720,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 *
 /* Construct an Authenticate-Results header portion, for the ARC module */
 
 gstring *