X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/978e78de01845b26817e297be793b86bf7aaa4dc..e30f4f43de211b14bd405a3d0e1579b9bd814908:/src/src/arc.c diff --git a/src/src/arc.c b/src/src/arc.c index 28438e65e..80bb30e03 100644 --- a/src/src/arc.c +++ b/src/src/arc.c @@ -21,6 +21,11 @@ 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 }; /******************************************************************************/ @@ -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"; @@ -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); @@ -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,14 +681,20 @@ 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) { @@ -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); @@ -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"; } @@ -942,16 +984,13 @@ return NULL; static const uschar * arc_verify_seals(arc_ctx * ctx) { -arc_set * as = ctx->arcset_chain; +arc_set * as = ctx->arcset_chain_last; if (!as) return US"none"; -while (as) - { - if (arc_seal_verify(ctx, as)) return US"fail"; - as = as->next; - } +for ( ; as; as = as->prev) if (arc_seal_verify(ctx, as)) return US"fail"; + DEBUG(D_acl) debug_printf("ARC: AS vfy overall pass\n"); return NULL; } @@ -966,9 +1005,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 +1022,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 +1040,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 +1082,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 +1118,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 * @@ -1209,7 +1254,10 @@ 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); + DEBUG(D_transport) + debug_printf("private key, or private-key file content, was: '%s'\n", + privkey); 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,7 +1416,7 @@ 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; @@ -1390,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; @@ -1418,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 */ @@ -1458,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. @@ -1485,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 @@ -1498,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; @@ -1507,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 @@ -1574,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 @@ -1589,11 +1696,13 @@ 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. */ -g = string_catn(g, sigheaders->s, sigheaders->ptr); +if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr); (void) string_from_gstring(g); gstring_reset_unused(g); return g; @@ -1675,7 +1784,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) @@ -1690,7 +1829,10 @@ if (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); @@ -1698,8 +1840,10 @@ if (arc_state) US" arc.oldest-pass=", string_sprintf("%d", arc_oldest_pass)); if (sender_host_address) - g = string_append(g, 2, US" smtp.client-ip=", sender_host_address); + g = string_append(g, 2, US" smtp.remote-ip=", sender_host_address); } + else if (arc_state_reason) + g = string_append(g, 3, US" (", arc_state_reason, US")"); DEBUG(D_acl) debug_printf("ARC: authres '%.*s'\n", g->ptr - start - 3, g->s + start + 3); }