1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
4 /* Experimental ARC support for Exim
5 Copyright (c) Jeremy Harris 2018
10 #ifdef EXPERIMENTAL_ARC
11 # if !defined SUPPORT_SPF
12 # error SPF must also be enabled for ARC
13 # elif defined DISABLE_DKIM
14 # error DKIM must also be enabled for ARC
17 # include "functions.h"
18 # include "pdkim/pdkim.h"
19 # include "pdkim/signing.h"
21 extern pdkim_ctx * dkim_verify_ctx;
22 extern pdkim_ctx dkim_sign_ctx;
24 #define ARC_SIGN_OPT_TSTAMP BIT(0)
25 #define ARC_SIGN_OPT_EXPIRE BIT(1)
27 #define ARC_SIGN_DEFAULT_EXPIRE_DELTA (60 * 60 * 24 * 30) /* one month */
29 /******************************************************************************/
31 typedef struct hdr_rlist {
32 struct hdr_rlist * prev;
37 typedef struct arc_line {
38 header_line * complete; /* including the header name; nul-term */
41 /* identified tag contents */
54 /* tag content sub-portions */
61 /* modified copy of b= field in line */
65 typedef struct arc_set {
66 struct arc_set * next;
67 struct arc_set * prev;
74 const uschar * ams_verify_done;
75 BOOL ams_verify_passed;
78 typedef struct arc_ctx {
79 arc_set * arcset_chain;
80 arc_set * arcset_chain_last;
83 #define ARC_HDR_AAR US"ARC-Authentication-Results:"
84 #define ARC_HDRLEN_AAR 27
85 #define ARC_HDR_AMS US"ARC-Message-Signature:"
86 #define ARC_HDRLEN_AMS 22
87 #define ARC_HDR_AS US"ARC-Seal:"
88 #define ARC_HDRLEN_AS 9
89 #define HDR_AR US"Authentication-Results:"
94 static hdr_rlist * headers_rlist;
95 static arc_ctx arc_sign_ctx = { NULL };
98 /******************************************************************************/
101 /* Get the instance number from the header.
104 arc_instance_from_hdr(const arc_line * al)
106 const uschar * s = al->i.data;
107 if (!s || !al->i.len) return 0;
108 return (unsigned) atoi(CCS s);
116 while (c && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) c = *++s;
121 /* Locate instance struct on chain, inserting a new one if
122 needed. The chain is in increasing-instance-number order
123 by the "next" link, and we have a "prev" link also.
127 arc_find_set(arc_ctx * ctx, unsigned i)
129 arc_set ** pas, * as, * next, * prev;
131 for (pas = &ctx->arcset_chain, prev = NULL, next = ctx->arcset_chain;
132 as = *pas; pas = &as->next)
134 if (as->instance > i) break;
135 if (as->instance == i)
137 DEBUG(D_acl) debug_printf("ARC: existing instance %u\n", i);
144 DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i);
145 *pas = as = store_get(sizeof(arc_set));
146 memset(as, 0, sizeof(arc_set));
153 ctx->arcset_chain_last = as;
159 /* Insert a tag content into the line structure.
160 Note this is a reference to existing data, not a copy.
161 Check for already-seen tag.
162 The string-pointer is on the '=' for entry. Update it past the
163 content (to the ;) on return;
167 arc_insert_tagvalue(arc_line * al, unsigned loff, uschar ** ss)
171 blob * b = (blob *)(US al + loff);
174 /* [FWS] tag-value [FWS] */
176 if (b->data) return US"fail";
177 s = skip_fws(s); /* FWS */
180 while ((c = *s) && c != ';') { len++; s++; }
182 while (len && ((c = s[-1]) == ' ' || c == '\t' || c == '\n' || c == '\r'))
183 { s--; len--; } /* FWS */
189 /* Inspect a header line, noting known tag fields.
190 Check for duplicates. */
193 arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only)
195 uschar * s = h->text + off;
196 uschar * r = NULL; /* compiler-quietening */
203 al->rawsig_no_b_val.data = store_get(h->slen + 1);
204 memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */
205 r = al->rawsig_no_b_val.data + off;
206 al->rawsig_no_b_val.len = off;
209 /* tag-list = tag-spec *( ";" tag-spec ) [ ";" ] */
216 uschar * fieldstart = s;
217 uschar * bstart = NULL, * bend;
219 /* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */
221 s = skip_fws(s); /* FWS */
223 /* debug_printf("%s: consider '%s'\n", __FUNCTION__, s); */
225 s = skip_fws(s); /* FWS */
228 if (!instance_only || tagchar == 'i') switch (tagchar)
230 case 'a': /* a= AMS algorithm */
232 if (*s != '=') return US"no 'a' value";
233 if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup";
235 /* substructure: algo-hash (eg. rsa-sha256) */
237 t = al->a_algo.data = al->a.data;
239 if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value";
241 if (*t++ != '-') return US"no '-' in 'a' value";
243 al->a_hash.len = al->a.len - i - 1;
252 case '=': /* b= AMS signature */
253 if (al->b.data) return US"already b data";
256 /* The signature can have FWS inserted in the content;
257 make a stripped copy */
259 while ((c = *++s) && c != ';')
260 if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
261 g = string_catn(g, s, 1);
262 al->b.data = string_from_gstring(g);
264 gstring_reset_unused(g);
267 case 'h': /* bh= AMS body hash */
268 s = skip_fws(++s); /* FWS */
269 if (*s != '=') return US"no bh value";
270 if (al->bh.data) return US"already bh data";
272 /* The bodyhash can have FWS inserted in the content;
273 make a stripped copy */
275 while ((c = *++s) && c != ';')
276 if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
277 g = string_catn(g, s, 1);
278 al->bh.data = string_from_gstring(g);
280 gstring_reset_unused(g);
290 case '=': /* c= AMS canonicalisation */
291 if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup";
293 /* substructure: head/body (eg. relaxed/simple)) */
295 t = al->c_head.data = al->c.data;
297 if (!*t++ || ++i > al->a.len) break;
299 if (*t++ == '/') /* /body is optional */
302 al->c_body.len = al->c.len - i - 1;
306 al->c_body.data = US"simple";
310 case 'v': /* cv= AS validity */
311 if (*++s != '=') return US"cv tag val";
312 if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) return US"cv tag dup";
318 case 'd': /* d= AMS domain */
319 if (*s != '=') return US"d tag val";
320 if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) return US"d tag dup";
322 case 'h': /* h= AMS headers */
323 if (*s != '=') return US"h tag val";
324 if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) return US"h tag dup";
326 case 'i': /* i= ARC set instance */
327 if (*s != '=') return US"i tag val";
328 if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) return US"i tag dup";
329 if (instance_only) goto done;
331 case 'l': /* l= bodylength */
332 if (*s != '=') return US"l tag val";
333 if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) return US"l tag dup";
335 case 's': /* s= AMS selector */
336 if (*s != '=') return US"s tag val";
337 if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) return US"s tag dup";
341 while ((c = *s) && c != ';') s++;
342 if (c) s++; /* ; after tag-spec */
344 /* for all but the b= tag, copy the field including FWS. For the b=,
345 drop the tag content. */
350 size_t n = bstart - fieldstart;
351 memcpy(r, fieldstart, n); /* FWS "b=" */
353 al->rawsig_no_b_val.len += n;
355 memcpy(r, bend, n); /* FWS ";" */
357 al->rawsig_no_b_val.len += n;
361 size_t n = s - fieldstart;
362 memcpy(r, fieldstart, n);
364 al->rawsig_no_b_val.len += n;
372 /* debug_printf("%s: finshed\n", __FUNCTION__); */
377 /* Insert one header line in the correct set of the chain,
378 adding instances as needed and checking for duplicate lines.
382 arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
387 arc_line * al = store_get(sizeof(arc_line)), ** alp;
390 memset(al, 0, sizeof(arc_line));
392 if ((e = arc_parse_line(al, h, off, instance_only)))
394 DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e);
395 return US"line parse";
397 if (!(i = arc_instance_from_hdr(al))) return US"instance find";
398 if (!(as = arc_find_set(ctx, i))) return US"set find";
399 if (*(alp = (arc_line **)(US as + hoff))) return US"dup hdr";
408 static const uschar *
409 arc_try_header(arc_ctx * ctx, header_line * h, BOOL instance_only)
413 /*debug_printf("consider hdr '%s'\n", h->text);*/
414 if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0)
420 for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; )
422 debug_printf("ARC: found AAR: %.*s\n", len, h->text);
424 if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar),
427 DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e);
428 return US"inserting AAR";
431 else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0)
439 for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; )
441 debug_printf("ARC: found AMS: %.*s\n", len, h->text);
443 if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams),
446 DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e);
447 return US"inserting AMS";
451 /*XXX dubious selection of ams here */
452 ams = ctx->arcset_chain->hdr_ams;
455 ams->c_head.data = US"simple"; ams->c_head.len = 6;
456 ams->c_body = ams->c_head;
459 else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0)
465 for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; )
467 debug_printf("ARC: found AS: %.*s\n", len, h->text);
469 if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as),
472 DEBUG(D_acl) debug_printf("inserting AS: %s\n", e);
473 return US"inserting AS";
481 /* Gather the chain of arc sets from the headers.
482 Check for duplicates while that is done. Also build the
483 reverse-order headers list;
485 Return: ARC state if determined, eg. by lack of any ARC chain.
488 static const uschar *
489 arc_vfy_collect_hdrs(arc_ctx * ctx)
492 hdr_rlist * r = NULL, * rprev = NULL;
495 DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n");
496 for (h = header_list; h; h = h->next)
498 r = store_get(sizeof(hdr_rlist));
504 if ((e = arc_try_header(ctx, h, FALSE)))
506 arc_state_reason = string_sprintf("collecting headers: %s", e);
512 if (!ctx->arcset_chain) return US"none";
518 arc_cv_match(arc_line * al, const uschar * s)
520 return Ustrncmp(s, al->cv.data, al->cv.len) == 0;
523 /******************************************************************************/
525 /* Return the hash of headers from the message that the AMS claims it
530 arc_get_verify_hhash(arc_ctx * ctx, arc_line * ams, blob * hhash)
532 const uschar * headernames = string_copyn(ams->h.data, ams->h.len);
536 BOOL relaxed = Ustrncmp(US"relaxed", ams->c_head.data, ams->c_head.len) == 0;
537 int hashtype = pdkim_hashname_to_hashtype(
538 ams->a_hash.data, ams->a_hash.len);
543 if (!exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
546 debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
550 /* For each headername in the list from the AMS (walking in order)
551 walk the message headers in reverse order, adding to the hash any
552 found for the first time. For that last point, maintain used-marks
553 on the list of message headers. */
555 DEBUG(D_acl) debug_printf("ARC: AMS header data for verification:\n");
557 for (r = headers_rlist; r; r = r->prev)
559 while ((hn = string_nextinlist(&headernames, &sep, NULL, 0)))
560 for (r = headers_rlist; r; r = r->prev)
562 && strncasecmp(CCS (s = r->h->text), CCS hn, Ustrlen(hn)) == 0
565 if (relaxed) s = pdkim_relax_header_n(s, r->h->slen, TRUE);
568 DEBUG(D_acl) pdkim_quoteprint(s, len);
569 exim_sha_update(&hhash_ctx, s, Ustrlen(s));
574 /* Finally add in the signature header (with the b= tag stripped); no CRLF */
576 s = ams->rawsig_no_b_val.data, len = ams->rawsig_no_b_val.len;
578 len = Ustrlen(s = pdkim_relax_header_n(s, len, FALSE));
579 DEBUG(D_acl) pdkim_quoteprint(s, len);
580 exim_sha_update(&hhash_ctx, s, len);
582 exim_sha_finish(&hhash_ctx, hhash);
584 { debug_printf("ARC: header hash: "); pdkim_hexprint(hhash->data, hhash->len); }
591 static pdkim_pubkey *
592 arc_line_to_pubkey(arc_line * al)
597 if (!(dns_txt = dkim_exim_query_dns_txt(string_sprintf("%.*s._domainkey.%.*s",
598 al->s.len, al->s.data, al->d.len, al->d.data))))
600 DEBUG(D_acl) debug_printf("pubkey dns lookup fail\n");
604 if ( !(p = pdkim_parse_pubkey_record(dns_txt))
605 || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0)
608 DEBUG(D_acl) debug_printf("pubkey dns lookup format error\n");
612 /* If the pubkey limits use to specified hashes, reject unusable
613 signatures. XXX should we have looked for multiple dns records? */
617 const uschar * list = p->hashes, * ele;
620 while ((ele = string_nextinlist(&list, &sep, NULL, 0)))
621 if (Ustrncmp(ele, al->a_hash.data, al->a_hash.len) == 0) break;
624 DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n",
625 p->hashes, (int)al->a.len, al->a.data);
635 static pdkim_bodyhash *
636 arc_ams_setup_vfy_bodyhash(arc_line * ams)
638 int canon_head, canon_body;
641 if (!ams->c.data) ams->c.data = US"simple"; /* RFC 6376 (DKIM) default */
642 pdkim_cstring_to_canons(ams->c.data, ams->c.len, &canon_head, &canon_body);
643 bodylen = ams->l.data
644 ? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) : -1;
646 return pdkim_set_bodyhash(dkim_verify_ctx,
647 pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len),
654 /* Verify an AMS. This is a DKIM-sig header, but with an ARC i= tag
655 and without a DKIM v= tag.
658 static const uschar *
659 arc_ams_verify(arc_ctx * ctx, arc_set * as)
661 arc_line * ams = as->hdr_ams;
668 const uschar * errstr;
670 as->ams_verify_done = US"in-progress";
672 /* Check the AMS has all the required tags:
676 "d=" domain (for key lookup)
677 "h=" headers (included in signature)
678 "s=" key-selector (for key lookup)
680 if ( !ams->a.data || !ams->b.data || !ams->bh.data || !ams->d.data
681 || !ams->h.data || !ams->s.data)
683 as->ams_verify_done = arc_state_reason = US"required tag missing";
688 /* The bodyhash should have been created earlier, and the dkim code should
689 have managed calculating it during message input. Find the reference to it. */
691 if (!(b = arc_ams_setup_vfy_bodyhash(ams)))
693 as->ams_verify_done = arc_state_reason = US"internal hash setup error";
699 debug_printf("ARC i=%d AMS Body bytes hashed: %lu\n"
700 " Body %.*s computed: ",
701 as->instance, b->signed_body_bytes,
702 (int)ams->a_hash.len, ams->a_hash.data);
703 pdkim_hexprint(CUS b->bh.data, b->bh.len);
706 /* We know the bh-tag blob is of a nul-term string, so safe as a string */
709 || (pdkim_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len)
710 || memcmp(sighash.data, b->bh.data, b->bh.len) != 0
715 debug_printf("ARC i=%d AMS Body hash from headers: ", as->instance);
716 pdkim_hexprint(sighash.data, sighash.len);
717 debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance);
719 return as->ams_verify_done = arc_state_reason = US"AMS body hash miscompare";
722 DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance);
724 /* Get the public key from DNS */
726 if (!(p = arc_line_to_pubkey(ams)))
727 return as->ams_verify_done = arc_state_reason = US"pubkey problem";
729 /* We know the b-tag blob is of a nul-term string, so safe as a string */
730 pdkim_decode_base64(ams->b.data, &sighash);
732 arc_get_verify_hhash(ctx, ams, &hhash);
734 /* Setup the interface to the signing library */
736 if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx)))
738 DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr);
739 as->ams_verify_done = arc_state_reason = US"internal sigverify init error";
743 hashtype = pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len);
745 if ((errstr = exim_dkim_verify(&vctx,
746 pdkim_hashes[hashtype].exim_hashmethod, &hhash, &sighash)))
748 DEBUG(D_acl) debug_printf("ARC i=%d AMS verify %s\n", as->instance, errstr);
749 return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify";
752 DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance);
753 as->ams_verify_passed = TRUE;
759 /* Check the sets are instance-continuous and that all
760 members are present. Check that no arc_seals are "fail".
761 Set the highest instance number global.
762 Verify the latest AMS.
765 arc_headers_check(arc_ctx * ctx)
769 BOOL ams_fail_found = FALSE;
771 if (!(as = ctx->arcset_chain))
774 for(inst = 0; as; as = as->next)
776 if ( as->instance != ++inst
777 || !as->hdr_aar || !as->hdr_ams || !as->hdr_as
778 || arc_cv_match(as->hdr_as, US"fail")
781 arc_state_reason = string_sprintf("i=%d"
782 " (cv, sequence or missing header)", as->instance);
783 DEBUG(D_acl) debug_printf("ARC chain fail at %s\n", arc_state_reason);
787 /* Evaluate the oldest-pass AMS validation while we're here.
788 It does not affect the AS chain validation but is reported as
792 if (arc_ams_verify(ctx, as))
793 ams_fail_found = TRUE;
795 arc_oldest_pass = inst;
796 arc_state_reason = NULL;
799 arc_received = ctx->arcset_chain_last;
800 arc_received_instance = inst;
802 /* We can skip the latest-AMS validation, if we already did it. */
804 as = ctx->arcset_chain_last;
805 if (!as->ams_verify_passed)
807 if (as->ams_verify_done)
809 arc_state_reason = as->ams_verify_done;
812 if (!!arc_ams_verify(ctx, as))
819 /******************************************************************************/
820 static const uschar *
821 arc_seal_verify(arc_ctx * ctx, arc_set * as)
823 arc_line * hdr_as = as->hdr_as;
831 const uschar * errstr;
833 DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance);
835 1. If the value of the "cv" tag on that seal is "fail", the
836 chain state is "fail" and the algorithm stops here. (This
837 step SHOULD be skipped if the earlier step (2.1) was
840 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv
841 == "none" && i != 1)) then the chain state is "fail" and the
842 algorithm stops here (note that the ordering of the logic is
843 structured for short-circuit evaluation).
846 if ( as->instance == 1 && !arc_cv_match(hdr_as, US"none")
847 || arc_cv_match(hdr_as, US"none") && as->instance != 1
850 arc_state_reason = US"seal cv state";
855 3. Initialize a hash function corresponding to the "a" tag of
859 hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len);
861 if (!exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
864 debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n");
865 arc_state_reason = US"seal hash setup error";
870 4. Compute the canonicalized form of the ARC header fields, in
871 the order described in Section 5.4.2, using the "relaxed"
872 header canonicalization defined in Section 3.4.2 of
873 [RFC6376]. Pass the canonicalized result to the hash
876 Headers are CRLF-separated, but the last one is not crlf-terminated.
879 DEBUG(D_acl) debug_printf("ARC: AS header data for verification:\n");
880 for (as2 = ctx->arcset_chain;
881 as2 && as2->instance <= as->instance;
889 if (!(s = al->relaxed))
890 al->relaxed = s = pdkim_relax_header_n(al->complete->text,
891 al->complete->slen, TRUE);
893 DEBUG(D_acl) pdkim_quoteprint(s, len);
894 exim_sha_update(&hhash_ctx, s, len);
897 if (!(s = al->relaxed))
898 al->relaxed = s = pdkim_relax_header_n(al->complete->text,
899 al->complete->slen, TRUE);
901 DEBUG(D_acl) pdkim_quoteprint(s, len);
902 exim_sha_update(&hhash_ctx, s, len);
905 if (as2->instance == as->instance)
906 s = pdkim_relax_header_n(al->rawsig_no_b_val.data,
907 al->rawsig_no_b_val.len, FALSE);
908 else if (!(s = al->relaxed))
909 al->relaxed = s = pdkim_relax_header_n(al->complete->text,
910 al->complete->slen, TRUE);
912 DEBUG(D_acl) pdkim_quoteprint(s, len);
913 exim_sha_update(&hhash_ctx, s, len);
917 5. Retrieve the final digest from the hash function.
920 exim_sha_finish(&hhash_ctx, &hhash_computed);
923 debug_printf("ARC i=%d AS Header %.*s computed: ",
924 as->instance, (int)hdr_as->a_hash.len, hdr_as->a_hash.data);
925 pdkim_hexprint(hhash_computed.data, hhash_computed.len);
930 6. Retrieve the public key identified by the "s" and "d" tags in
931 the ARC-Seal, as described in Section 4.1.6.
934 if (!(p = arc_line_to_pubkey(hdr_as)))
935 return US"pubkey problem";
938 7. Determine whether the signature portion ("b" tag) of the ARC-
939 Seal and the digest computed above are valid according to the
940 public key. (See also Section Section 8.4 for failure case
943 8. If the signature is not valid, the chain state is "fail" and
944 the algorithm stops here.
947 /* We know the b-tag blob is of a nul-term string, so safe as a string */
948 pdkim_decode_base64(hdr_as->b.data, &sighash);
950 if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx)))
952 DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr);
956 hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len);
958 if ((errstr = exim_dkim_verify(&vctx,
959 pdkim_hashes[hashtype].exim_hashmethod,
960 &hhash_computed, &sighash)))
963 debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr);
964 arc_state_reason = US"seal sigverify error";
968 DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d pass\n", as->instance);
973 static const uschar *
974 arc_verify_seals(arc_ctx * ctx)
976 arc_set * as = ctx->arcset_chain;
983 if (arc_seal_verify(ctx, as)) return US"fail";
986 DEBUG(D_acl) debug_printf("ARC: AS vfy overall pass\n");
989 /******************************************************************************/
991 /* Do ARC verification. Called from DATA ACL, on a verify = arc
992 condition. No arguments; we are checking globals.
994 Return: The ARC state, or NULL on error.
1000 arc_ctx ctx = { NULL };
1003 if (!dkim_verify_ctx)
1005 DEBUG(D_acl) debug_printf("ARC: no DKIM verify context\n");
1009 /* AS evaluation, per
1010 https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6
1012 /* 1. Collect all ARC sets currently on the message. If there were
1013 none, the ARC state is "none" and the algorithm stops here.
1016 if ((res = arc_vfy_collect_hdrs(&ctx)))
1019 /* 2. If the form of any ARC set is invalid (e.g., does not contain
1020 exactly one of each of the three ARC-specific header fields),
1021 then the chain state is "fail" and the algorithm stops here.
1023 1. To avoid the overhead of unnecessary computation and delay
1024 from crypto and DNS operations, the cv value for all ARC-
1025 Seal(s) MAY be checked at this point. If any of the values
1026 are "fail", then the overall state of the chain is "fail" and
1027 the algorithm stops here.
1029 3. Conduct verification of the ARC-Message-Signature header field
1030 bearing the highest instance number. If this verification fails,
1031 then the chain state is "fail" and the algorithm stops here.
1034 if ((res = arc_headers_check(&ctx)))
1037 /* 4. For each ARC-Seal from the "N"th instance to the first, apply the
1040 1. If the value of the "cv" tag on that seal is "fail", the
1041 chain state is "fail" and the algorithm stops here. (This
1042 step SHOULD be skipped if the earlier step (2.1) was
1045 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv
1046 == "none" && i != 1)) then the chain state is "fail" and the
1047 algorithm stops here (note that the ordering of the logic is
1048 structured for short-circuit evaluation).
1050 3. Initialize a hash function corresponding to the "a" tag of
1053 4. Compute the canonicalized form of the ARC header fields, in
1054 the order described in Section 5.4.2, using the "relaxed"
1055 header canonicalization defined in Section 3.4.2 of
1056 [RFC6376]. Pass the canonicalized result to the hash
1059 5. Retrieve the final digest from the hash function.
1061 6. Retrieve the public key identified by the "s" and "d" tags in
1062 the ARC-Seal, as described in Section 4.1.6.
1064 7. Determine whether the signature portion ("b" tag) of the ARC-
1065 Seal and the digest computed above are valid according to the
1066 public key. (See also Section Section 8.4 for failure case
1069 8. If the signature is not valid, the chain state is "fail" and
1070 the algorithm stops here.
1072 5. If all seals pass validation, then the chain state is "pass", and
1073 the algorithm is complete.
1076 if ((res = arc_verify_seals(&ctx)))
1085 /******************************************************************************/
1087 /* Prepend the header to the rlist */
1090 arc_rlist_entry(hdr_rlist * list, const uschar * s, int len)
1092 hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line));
1093 header_line * h = r->h = (header_line *)(r+1);
1102 /* This works for either NL or CRLF lines; also nul-termination */
1104 if (*s == '\n' && s[1] != '\t' && s[1] != ' ') break;
1105 s++; /* move past end of line */
1111 /* Walk the given headers strings identifying each header, and construct
1112 a reverse-order list.
1116 arc_sign_scan_headers(arc_ctx * ctx, gstring * sigheaders)
1119 hdr_rlist * rheaders = NULL;
1121 s = sigheaders ? sigheaders->s : NULL;
1124 const uschar * s2 = s;
1126 /* This works for either NL or CRLF lines; also nul-termination */
1128 if (*s2 == '\n' && s2[1] != '\t' && s2[1] != ' ') break;
1129 s2++; /* move past end of line */
1131 rheaders = arc_rlist_entry(rheaders, s, s2 - s);
1139 /* Return the A-R content, without identity, with line-ending and
1143 arc_sign_find_ar(header_line * headers, const uschar * identity, blob * ret)
1146 int ilen = Ustrlen(identity);
1149 for(h = headers; h; h = h->next)
1151 uschar * s = h->text, c;
1154 if (Ustrncmp(s, HDR_AR, HDRLEN_AR) != 0) continue;
1155 s += HDRLEN_AR, len -= HDRLEN_AR; /* header name */
1157 && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n'))
1158 s++, len--; /* FWS */
1159 if (Ustrncmp(s, identity, ilen) != 0) continue;
1160 s += ilen; len -= ilen; /* identity */
1161 if (len <= 0) continue;
1162 if ((c = *s) && c == ';') s++, len--; /* identity terminator */
1164 && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n'))
1165 s++, len--; /* FWS */
1166 if (len <= 0) continue;
1176 /* Append a constructed AAR including CRLF. Add it to the arc_ctx too. */
1179 arc_sign_append_aar(gstring * g, arc_ctx * ctx,
1180 const uschar * identity, int instance, blob * ar)
1182 int aar_off = g ? g->ptr : 0;
1183 arc_set * as = store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line));
1184 arc_line * al = (arc_line *)(as+1);
1185 header_line * h = (header_line *)(al+1);
1187 g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR);
1188 g = string_cat(g, string_sprintf(" i=%d; %s;\r\n\t", instance, identity));
1189 g = string_catn(g, US ar->data, ar->len);
1191 h->slen = g->ptr - aar_off;
1192 h->text = g->s + aar_off;
1195 as->prev = ctx->arcset_chain_last;
1196 as->instance = instance;
1199 ctx->arcset_chain = as;
1201 ctx->arcset_chain_last->next = as;
1202 ctx->arcset_chain_last = as;
1204 DEBUG(D_transport) debug_printf("ARC: AAR '%.*s'\n", h->slen - 2, h->text);
1211 arc_sig_from_pseudoheader(gstring * hdata, int hashtype, const uschar * privkey,
1212 blob * sig, const uschar * why)
1214 hashmethod hm = /*sig->keytype == KEYTYPE_ED25519*/ FALSE
1215 ? HASH_SHA2_512 : pdkim_hashes[hashtype].exim_hashmethod;
1218 const uschar * errstr;
1223 debug_printf("ARC: %s header data for signing:\n", why);
1224 pdkim_quoteprint(hdata->s, hdata->ptr);
1226 (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod);
1227 exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr);
1228 exim_sha_finish(&hhash_ctx, &hhash);
1229 debug_printf("ARC: header hash: "); pdkim_hexprint(hhash.data, hhash.len);
1232 if (FALSE /*need hash for Ed25519 or GCrypt signing*/ )
1235 (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod);
1236 exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr);
1237 exim_sha_finish(&hhash_ctx, &hhash);
1241 hhash.data = hdata->s;
1242 hhash.len = hdata->ptr;
1245 if ( (errstr = exim_dkim_signing_init(privkey, &sctx))
1246 || (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig)))
1248 log_write(0, LOG_MAIN, "ARC: %s signing: %s\n", why, errstr);
1257 arc_sign_append_sig(gstring * g, blob * sig)
1259 /*debug_printf("%s: raw sig ", __FUNCTION__); pdkim_hexprint(sig->data, sig->len);*/
1260 sig->data = pdkim_encode_base64(sig);
1261 sig->len = Ustrlen(sig->data);
1264 int len = MIN(sig->len, 74);
1265 g = string_catn(g, sig->data, len);
1266 if ((sig->len -= len) == 0) break;
1268 g = string_catn(g, US"\r\n\t ", 5);
1270 g = string_catn(g, US";\r\n", 3);
1271 gstring_reset_unused(g);
1272 string_from_gstring(g);
1277 /* Append a constructed AMS including CRLF. Add it to the arc_ctx too. */
1280 arc_sign_append_ams(gstring * g, arc_ctx * ctx, int instance,
1281 const uschar * identity, const uschar * selector, blob * bodyhash,
1282 hdr_rlist * rheaders, const uschar * privkey, unsigned options)
1285 gstring * hdata = NULL;
1287 int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */
1290 arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line));
1291 header_line * h = (header_line *)(al+1);
1293 /* debug_printf("%s\n", __FUNCTION__); */
1295 /* Construct the to-be-signed AMS pseudo-header: everything but the sig. */
1298 g = string_append(g, 7,
1300 US" i=", string_sprintf("%d", instance),
1301 US"; a=rsa-sha256; c=relaxed; d=", identity, /*XXX hardwired */
1302 US"; s=", selector);
1303 if (options & ARC_SIGN_OPT_TSTAMP)
1304 g = string_append(g, 2,
1305 US"; t=", string_sprintf("%lu", (u_long)now));
1306 if (options & ARC_SIGN_OPT_EXPIRE)
1307 g = string_append(g, 2,
1308 US"; x=", string_sprintf("%lu", (u_long)expire));
1309 g = string_append(g, 3,
1310 US";\r\n\tbh=", pdkim_encode_base64(bodyhash),
1313 for(col = 3; rheaders; rheaders = rheaders->prev)
1315 const uschar * hnames = US"DKIM-Signature:" PDKIM_DEFAULT_SIGN_HEADERS;
1316 uschar * name, * htext = rheaders->h->text;
1319 /* Spot headers of interest */
1321 while ((name = string_nextinlist(&hnames, &sep, NULL, 0)))
1323 int len = Ustrlen(name);
1324 if (strncasecmp(CCS htext, CCS name, len) == 0)
1326 /* If too long, fold line in h= field */
1328 if (col + len > 78) g = string_catn(g, US"\r\n\t ", 5), col = 3;
1330 /* Add name to h= list */
1332 g = string_catn(g, name, len);
1333 g = string_catn(g, US":", 1);
1336 /* Accumulate header for hashing/signing */
1338 hdata = string_cat(hdata,
1339 pdkim_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */
1345 /* Lose the last colon from the h= list */
1347 if (g->s[g->ptr - 1] == ':') g->ptr--;
1349 g = string_catn(g, US";\r\n\tb=;", 7);
1351 /* Include the pseudo-header in the accumulation */
1353 s = pdkim_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE);
1354 hdata = string_cat(hdata, s);
1356 /* Calculate the signature from the accumulation */
1357 /*XXX does that need further relaxation? there are spaces embedded in the b= strings! */
1359 if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AMS"))
1362 /* Lose the trailing semicolon from the psuedo-header, and append the signature
1363 (folded over lines) and termination to complete it. */
1366 g = arc_sign_append_sig(g, &sig);
1368 h->slen = g->ptr - ams_off;
1369 h->text = g->s + ams_off;
1371 ctx->arcset_chain_last->hdr_ams = al;
1373 DEBUG(D_transport) debug_printf("ARC: AMS '%.*s'\n", h->slen - 2, h->text);
1379 /* Look for an arc= result in an A-R header blob. We know that its data
1380 happens to be a NUL-term string. */
1383 arc_ar_cv_status(blob * ar)
1385 const uschar * resinfo = ar->data;
1387 uschar * methodspec, * s;
1389 while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0)))
1390 if (Ustrncmp(methodspec, US"arc=", 4) == 0)
1393 for (s = methodspec += 4;
1394 (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++;
1395 return string_copyn(methodspec, s - methodspec);
1402 /* Build the AS header and prepend it */
1405 arc_sign_prepend_as(gstring * arcset_interim, arc_ctx * ctx,
1406 int instance, const uschar * identity, const uschar * selector, blob * ar,
1407 const uschar * privkey, unsigned options)
1411 uschar * status = arc_ar_cv_status(ar);
1412 arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line));
1413 header_line * h = (header_line *)(al+1);
1415 gstring * hdata = NULL;
1416 int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */
1422 - no h= tag; implicit coverage
1423 - arc status from A-R
1425 - coverage is just the new ARC set
1426 including self (but with an empty b= in self)
1428 - all ARC set headers, set-number order, aar then ams then as,
1429 including self (but with an empty b= in self)
1432 /* Construct the AS except for the signature */
1434 arcset = string_append(NULL, 9,
1436 US" i=", string_sprintf("%d", instance),
1438 US"; a=rsa-sha256; d=", identity, /*XXX hardwired */
1439 US"; s=", selector); /*XXX same as AMS */
1440 if (options & ARC_SIGN_OPT_TSTAMP)
1441 arcset = string_append(arcset, 2,
1442 US"; t=", string_sprintf("%lu", (u_long)now));
1443 arcset = string_cat(arcset,
1446 h->slen = arcset->ptr;
1447 h->text = arcset->s;
1449 ctx->arcset_chain_last->hdr_as = al;
1451 /* For any but "fail" chain-verify status, walk the entire chain in order by
1452 instance. For fail, only the new arc-set. Accumulate the elements walked. */
1454 for (as = Ustrcmp(status, US"fail") == 0
1455 ? ctx->arcset_chain_last : ctx->arcset_chain;
1458 /* Accumulate AAR then AMS then AS. Relaxed canonicalisation
1459 is required per standard. */
1461 h = as->hdr_aar->complete;
1462 hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
1463 h = as->hdr_ams->complete;
1464 hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE));
1465 h = as->hdr_as->complete;
1466 hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, !!as->next));
1469 /* Calculate the signature from the accumulation */
1471 if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AS"))
1474 /* Lose the trailing semicolon */
1476 arcset = arc_sign_append_sig(arcset, &sig);
1477 DEBUG(D_transport) debug_printf("ARC: AS '%.*s'\n", arcset->ptr - 2, arcset->s);
1479 /* Finally, append the AMS and AAR to the new AS */
1481 return string_catn(arcset, arcset_interim->s, arcset_interim->ptr);
1485 /**************************************/
1487 /* Return pointer to pdkim_bodyhash for given hash method, creating new
1492 arc_ams_setup_sign_bodyhash(void)
1494 int canon_head, canon_body;
1496 DEBUG(D_transport) debug_printf("ARC: requesting bodyhash\n");
1497 pdkim_cstring_to_canons(US"relaxed", 7, &canon_head, &canon_body); /*XXX hardwired */
1498 return pdkim_set_bodyhash(&dkim_sign_ctx,
1499 pdkim_hashname_to_hashtype(US"sha256", 6), /*XXX hardwired */
1509 memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx));
1514 /* A "normal" header line, identified by DKIM processing. These arrive before
1515 the call to arc_sign(), which carries any newly-created DKIM headers - and
1516 those go textually before the normal ones in the message.
1518 We have to take the feed from DKIM as, in the transport-filter case, the
1519 headers are not in memory at the time of the call to arc_sign().
1521 Take a copy of the header and construct a reverse-order list.
1522 Also parse ARC-chain headers and build the chain struct, retaining pointers
1526 static const uschar *
1527 arc_header_sign_feed(gstring * g)
1529 uschar * s = string_copyn(g->s, g->ptr);
1530 headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr);
1531 return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE);
1536 /* ARC signing. Called from the smtp transport, if the arc_sign option is set.
1537 The dkim_exim_sign() function has already been called, so will have hashed the
1538 message body for us so long as we requested a hash previously.
1541 signspec Three-element colon-sep list: identity, selector, privkey.
1542 Optional fourth element: comma-sep list of options.
1544 sigheaders Any signature headers already generated, eg. by DKIM, or NULL
1548 Set of headers to prepend to the message, including the supplied sigheaders
1549 but not the plainheaders.
1553 arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr)
1555 const uschar * identity, * selector, * privkey, * opts, * s;
1556 unsigned options = 0;
1558 header_line * headers;
1559 hdr_rlist * rheaders;
1567 /* Parse the signing specification */
1569 identity = string_nextinlist(&signspec, &sep, NULL, 0);
1570 selector = string_nextinlist(&signspec, &sep, NULL, 0);
1571 if ( !*identity || !*selector
1572 || !(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey)
1574 log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)",
1575 !*identity ? "identity" : !*selector ? "selector" : "private-key");
1576 return sigheaders ? sigheaders : string_get(0);
1578 if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey)))
1579 return sigheaders ? sigheaders : string_get(0);
1581 if ((opts = string_nextinlist(&signspec, &sep, NULL, 0)))
1584 while ((s = string_nextinlist(&opts, &osep, NULL, 0)))
1585 if (Ustrcmp(s, "timestamps") == 0)
1587 options |= ARC_SIGN_OPT_TSTAMP;
1588 if (!now) now = time(NULL);
1590 else if (Ustrncmp(s, "expire", 6) == 0)
1592 options |= ARC_SIGN_OPT_EXPIRE;
1593 if (*(s += 6) == '=')
1596 if (!(expire = (time_t)atoi(++s)))
1597 expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA;
1598 if (!now) now = time(NULL);
1602 expire = (time_t)atol(s);
1605 if (!now) now = time(NULL);
1606 expire = now + ARC_SIGN_DEFAULT_EXPIRE_DELTA;
1611 DEBUG(D_transport) debug_printf("ARC: sign for %s\n", identity);
1613 /* Make an rlist of any new DKIM headers, then add the "normals" rlist to it.
1614 Then scan the list for an A-R header. */
1616 string_from_gstring(sigheaders);
1617 if ((rheaders = arc_sign_scan_headers(&arc_sign_ctx, sigheaders)))
1620 for (rp = &headers_rlist; *rp; ) rp = &(*rp)->prev;
1624 /* Finally, build a normal-order headers list */
1625 /*XXX only needed for hunt-the-AR? */
1626 /*XXX also, we really should be accepting any number of ADMD-matching ARs */
1628 header_line * hnext = NULL;
1629 for (rheaders = headers_rlist; rheaders;
1630 hnext = rheaders->h, rheaders = rheaders->prev)
1631 rheaders->h->next = hnext;
1635 if (!(arc_sign_find_ar(headers, identity, &ar)))
1637 log_write(0, LOG_MAIN, "ARC: no Authentication-Results header for signing");
1638 return sigheaders ? sigheaders : string_get(0);
1641 /* We previously built the data-struct for the existing ARC chain, if any, using a headers
1642 feed from the DKIM module. Use that to give the instance number for the ARC set we are
1646 if (arc_sign_ctx.arcset_chain_last)
1647 debug_printf("ARC: existing chain highest instance: %d\n",
1648 arc_sign_ctx.arcset_chain_last->instance);
1650 debug_printf("ARC: no existing chain\n");
1652 instance = arc_sign_ctx.arcset_chain_last ? arc_sign_ctx.arcset_chain_last->instance + 1 : 1;
1656 - copy the A-R; prepend i= & identity
1659 g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar);
1663 - Looks fairly like a DKIM sig
1664 - Cover all DKIM sig headers as well as the usuals
1667 - we must have requested a suitable bodyhash previously
1670 b = arc_ams_setup_sign_bodyhash();
1671 g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector,
1672 &b->bh, headers_rlist, privkey, options);
1677 - no h= tag; implicit coverage
1678 - arc status from A-R
1680 - coverage is just the new ARC set
1681 including self (but with an empty b= in self)
1683 - all ARC set headers, set-number order, aar then ams then as,
1684 including self (but with an empty b= in self)
1687 g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar,
1690 /* Finally, append the dkim headers and return the lot. */
1692 g = string_catn(g, sigheaders->s, sigheaders->ptr);
1693 (void) string_from_gstring(g);
1694 gstring_reset_unused(g);
1699 /******************************************************************************/
1701 /* Check to see if the line is an AMS and if so, set up to validate it.
1702 Called from the DKIM input processing. This must be done now as the message
1703 body data is hashed during input.
1705 We call the DKIM code to request a body-hash; it has the facility already
1706 and the hash parameters might be common with other requests.
1709 static const uschar *
1710 arc_header_vfy_feed(gstring * g)
1717 if (!dkim_verify_ctx) return US"no dkim context";
1719 if (strncmpic(ARC_HDR_AMS, g->s, ARC_HDRLEN_AMS) != 0) return US"not AMS";
1721 DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n");
1722 /* Parse the AMS header */
1727 memset(&al, 0, sizeof(arc_line));
1728 if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE)))
1730 DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr);
1731 return US"line parsing error";
1737 al.c_body.data = US"simple"; al.c_body.len = 6;
1738 al.c_head = al.c_body;
1741 /* Ask the dkim code to calc a bodyhash with those specs */
1743 if (!(b = arc_ams_setup_vfy_bodyhash(&al)))
1744 return US"dkim hash setup fail";
1746 /* Discard the reference; search again at verify time, knowing that one
1747 should have been created here. */
1754 /* A header line has been identified by DKIM processing.
1758 is_vfy TRUE for verify mode or FALSE for signing mode
1761 NULL for success, or an error string (probably unused)
1765 arc_header_feed(gstring * g, BOOL is_vfy)
1767 return is_vfy ? arc_header_vfy_feed(g) : arc_header_sign_feed(g);
1772 /******************************************************************************/
1774 /* Construct an Authenticate-Results header portion, for the ARC module */
1777 authres_arc(gstring * g)
1781 arc_line * highest_ams;
1782 int start = 0; /* Compiler quietening */
1783 DEBUG(D_acl) start = g->ptr;
1785 g = string_append(g, 2, US";\n\tarc=", arc_state);
1786 if (arc_received_instance > 0)
1788 g = string_append(g, 3, US" (i=",
1789 string_sprintf("%d", arc_received_instance), US")");
1790 if (arc_state_reason)
1791 g = string_append(g, 3, US"(", arc_state_reason, US")");
1792 g = string_catn(g, US" header.s=", 10);
1793 highest_ams = arc_received->hdr_ams;
1794 g = string_catn(g, highest_ams->s.data, highest_ams->s.len);
1796 g = string_append(g, 2,
1797 US" arc.oldest-pass=", string_sprintf("%d", arc_oldest_pass));
1799 if (sender_host_address)
1800 g = string_append(g, 2, US" smtp.client-ip=", sender_host_address);
1802 else if (arc_state_reason)
1803 g = string_append(g, 3, US" (", arc_state_reason, US")");
1804 DEBUG(D_acl) debug_printf("ARC: authres '%.*s'\n",
1805 g->ptr - start - 3, g->s + start + 3);
1808 DEBUG(D_acl) debug_printf("ARC: no authres\n");
1813 # endif /* SUPPORT_SPF */
1814 #endif /* EXPERIMENTAL_ARC */