diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b316e..fdc894a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ## Unreleased ### Added +- `oldest-pass` processing per [RFC 8617 section 5.2](https://datatracker.ietf.org/doc/html/rfc8617#section-5.2). +- libopenarc - `arc_chain_oldest_pass()` - milter - `AuthResIP` configuration option. - milter - `RequireSafeKeys` configuration option. @@ -22,6 +24,8 @@ All notable changes to this project will be documented in this file. are excluded from the AMS, as required by RFC 8617. - libopenarc - ARC headers are returned with a space before the header value. - libopenarc - String arguments are marked as `const` where applicable. +- milter - `Authentication-Results` and `ARC-Authentication-Results` include + `header.oldest-pass` when appropriate. ### Fixed - libopenarc - Seals on failed chains only cover the latest ARC header set, @@ -32,6 +36,8 @@ All notable changes to this project will be documented in this file. - libopenarc - The installed pkg-config file is more correct. - libopenarc - U-labels (domain labels encoded as UTF-8) are allowed in `d=` and `s=` tags. +- libopenarc - `arc_eom()` propagates internal errors like memory allocation + failure instead of marking the chain as failed. - milter - Use after free. - milter - Unlikely division by zero. - milter - Small memory leak during config loading. diff --git a/libopenarc/arc-canon.c b/libopenarc/arc-canon.c index 38fcd68..df2dbbd 100644 --- a/libopenarc/arc-canon.c +++ b/libopenarc/arc-canon.c @@ -684,12 +684,16 @@ arc_add_canon(ARC_MESSAGE *msg, assert(hashtype == ARC_HASHTYPE_SHA1 || hashtype == ARC_HASHTYPE_SHA256); - if (type == ARC_CANONTYPE_HEADER) + /* Body canons can be shared if the parameters match. Header canons could + * theoretically be partially shared if the `h` tags match, but it would be + * complex so we don't currently do it. */ + if (type == ARC_CANONTYPE_BODY) { for (cur = msg->arc_canonhead; cur != NULL; cur = cur->canon_next) { - if (cur->canon_type != ARC_CANONTYPE_HEADER || - cur->canon_hashtype != hashtype || cur->canon_length != length) + if (cur->canon_type != ARC_CANONTYPE_BODY || + cur->canon_canon != canon || cur->canon_hashtype != hashtype || + cur->canon_length != length) { continue; } @@ -1947,6 +1951,7 @@ arc_canon_getsealhash(ARC_MESSAGE *msg, int setnum, void **sh, size_t *shlen) ** ** Parameters: ** msg -- ARC message from which to get completed hashes +** setnum -- which seal's hashes to get ** hh -- pointer to header hash buffer (returned) ** hhlen -- bytes used at hh (returned) ** bh -- pointer to body hash buffer (returned) @@ -1958,8 +1963,12 @@ arc_canon_getsealhash(ARC_MESSAGE *msg, int setnum, void **sh, size_t *shlen) */ ARC_STAT -arc_canon_gethashes( - ARC_MESSAGE *msg, void **hh, size_t *hhlen, void **bh, size_t *bhlen) +arc_canon_gethashes(ARC_MESSAGE *msg, + int setnum, + void **hh, + size_t *hhlen, + void **bh, + size_t *bhlen) { ARC_STAT status; struct arc_canon *hdc; @@ -1969,8 +1978,8 @@ arc_canon_gethashes( size_t hdlen; size_t bdlen; - hdc = msg->arc_valid_hdrcanon; - bdc = msg->arc_valid_bodycanon; + hdc = msg->arc_hdrcanons[setnum - 1]; + bdc = msg->arc_bodycanons[setnum - 1]; status = arc_canon_getfinal(hdc, &hd, &hdlen); if (status != ARC_STAT_OK) diff --git a/libopenarc/arc-canon.h b/libopenarc/arc-canon.h index 60dfdf3..84130da 100644 --- a/libopenarc/arc-canon.h +++ b/libopenarc/arc-canon.h @@ -41,7 +41,7 @@ extern void arc_canon_cleanup(ARC_MESSAGE *); extern ARC_STAT arc_canon_closebody(ARC_MESSAGE *); extern ARC_STAT arc_canon_getfinal(ARC_CANON *, unsigned char **, size_t *); extern ARC_STAT arc_canon_gethashes( - ARC_MESSAGE *, void **, size_t *, void **, size_t *); + ARC_MESSAGE *, int, void **, size_t *, void **, size_t *); extern ARC_STAT arc_canon_getsealhash(ARC_MESSAGE *, int, void **, size_t *); extern ARC_STAT arc_canon_header_string( struct arc_dstring *, arc_canon_t, const char *, size_t, bool); diff --git a/libopenarc/arc-types.h b/libopenarc/arc-types.h index a3165d6..b95e4c1 100644 --- a/libopenarc/arc-types.h +++ b/libopenarc/arc-types.h @@ -121,6 +121,7 @@ struct arc_msghandle bool arc_infail; int arc_dnssec_key; int arc_signalg; + int arc_oldest_pass; unsigned int arc_mode; unsigned int arc_nsets; unsigned int arc_margin; @@ -158,9 +159,9 @@ struct arc_msghandle struct arc_dstring *arc_hdrbuf; struct arc_canon *arc_sealcanon; struct arc_canon **arc_sealcanons; - struct arc_canon *arc_valid_hdrcanon; + struct arc_canon **arc_hdrcanons; + struct arc_canon **arc_bodycanons; struct arc_canon *arc_sign_hdrcanon; - struct arc_canon *arc_valid_bodycanon; struct arc_canon *arc_sign_bodycanon; struct arc_canon *arc_canonhead; struct arc_canon *arc_canontail; diff --git a/libopenarc/arc.c b/libopenarc/arc.c index 7ecf103..afa2216 100644 --- a/libopenarc/arc.c +++ b/libopenarc/arc.c @@ -2280,7 +2280,7 @@ arc_validate_msg(ARC_MESSAGE *msg, unsigned int setnum) } /* extract the header and body hashes from the message */ - status = arc_canon_gethashes(msg, &hh, &hhlen, &bh, &bhlen); + status = arc_canon_gethashes(msg, setnum, &hh, &hhlen, &bh, &bhlen); if (status != ARC_STAT_OK) { arc_error(msg, "arc_canon_gethashes() failed"); @@ -2763,9 +2763,10 @@ arc_eoh_verify(ARC_MESSAGE *msg) { unsigned int n; unsigned int hashtype; + char *c; ARC_STAT status; - struct arc_hdrfield *h; - char *htag; + struct arc_hdrfield *h = NULL; + char *htag = NULL; arc_canon_t hdr_canon; arc_canon_t body_canon; @@ -2775,18 +2776,51 @@ arc_eoh_verify(ARC_MESSAGE *msg) return ARC_STAT_OK; } + if (msg->arc_nsets == 0) + { + return ARC_STAT_OK; + } + /* ** Request specific canonicalizations we want to run. */ - h = NULL; - htag = NULL; - if (msg->arc_nsets > 0) + /* sets already in the chain, validation */ + msg->arc_sealcanons = ARC_MALLOC(msg->arc_nsets * sizeof(ARC_CANON *)); + msg->arc_hdrcanons = ARC_MALLOC(msg->arc_nsets * sizeof(ARC_CANON *)); + msg->arc_bodycanons = ARC_MALLOC(msg->arc_nsets * sizeof(ARC_CANON *)); + + if (msg->arc_sealcanons == NULL || msg->arc_hdrcanons == NULL || + msg->arc_bodycanons == NULL) { - char *c; + arc_error(msg, "failed to allocate memory for canonicalizations"); + return ARC_STAT_INTERNAL; + } + + for (n = 0; n < msg->arc_nsets; n++) + { + h = msg->arc_sets[n].arcset_as; - /* headers, validation */ - h = msg->arc_sets[msg->arc_nsets - 1].arcset_ams; + if (strcmp(arc_param_get(h->hdr_data, "a"), "rsa-sha1") == 0) + { + hashtype = ARC_HASHTYPE_SHA1; + } + else + { + hashtype = ARC_HASHTYPE_SHA256; + } + + status = arc_add_canon(msg, ARC_CANONTYPE_SEAL, ARC_CANON_RELAXED, + hashtype, NULL, h, (ssize_t) -1, + &msg->arc_sealcanons[n]); + if (status != ARC_STAT_OK) + { + arc_error(msg, "failed to initialize seal canonicalization object"); + return status; + } + + /* AMS */ + h = msg->arc_sets[n].arcset_ams; htag = arc_param_get(h->hdr_data, "h"); if (strcmp(arc_param_get(h->hdr_data, "a"), "rsa-sha1") == 0) { @@ -2817,7 +2851,7 @@ arc_eoh_verify(ARC_MESSAGE *msg) } status = arc_add_canon(msg, ARC_CANONTYPE_HEADER, hdr_canon, hashtype, - htag, h, (ssize_t) -1, &msg->arc_valid_hdrcanon); + htag, h, (ssize_t) -1, &msg->arc_hdrcanons[n]); if (status != ARC_STAT_OK) { @@ -2829,7 +2863,7 @@ arc_eoh_verify(ARC_MESSAGE *msg) /* body, validation */ status = arc_add_canon(msg, ARC_CANONTYPE_BODY, body_canon, hashtype, NULL, NULL, (ssize_t) -1, - &msg->arc_valid_bodycanon); + &msg->arc_bodycanons[n]); if (status != ARC_STAT_OK) { @@ -2838,42 +2872,6 @@ arc_eoh_verify(ARC_MESSAGE *msg) } } - /* sets already in the chain, validation */ - if (msg->arc_nsets > 0) - { - msg->arc_sealcanons = ARC_MALLOC(msg->arc_nsets * sizeof(ARC_CANON *)); - if (msg->arc_sealcanons == NULL) - { - arc_error(msg, "failed to allocate memory for canonicalizations"); - return status; - } - - for (n = 0; n < msg->arc_nsets; n++) - { - h = msg->arc_sets[n].arcset_as; - - int hashtype; - if (strcmp(arc_param_get(h->hdr_data, "a"), "rsa-sha1") == 0) - { - hashtype = ARC_HASHTYPE_SHA1; - } - else - { - hashtype = ARC_HASHTYPE_SHA256; - } - - status = arc_add_canon(msg, ARC_CANONTYPE_SEAL, ARC_CANON_RELAXED, - hashtype, NULL, h, (ssize_t) -1, - &msg->arc_sealcanons[n]); - if (status != ARC_STAT_OK) - { - arc_error(msg, - "failed to initialize seal canonicalization object"); - return status; - } - } - } - return ARC_STAT_OK; } @@ -3162,11 +3160,6 @@ arc_body(ARC_MESSAGE *msg, const unsigned char *buf, size_t len) assert(msg != NULL); assert(buf != NULL); - if (msg->arc_state == ARC_CHAIN_FAIL) - { - return ARC_STAT_OK; - } - if (msg->arc_state > ARC_STATE_BODY || msg->arc_state < ARC_STATE_EOH) { return ARC_STAT_INVALID; @@ -3189,8 +3182,10 @@ arc_body(ARC_MESSAGE *msg, const unsigned char *buf, size_t len) ARC_STAT arc_eom(ARC_MESSAGE *msg) { + ARC_STAT status; + /* nothing to do if the chain has been expressly failed */ - if (msg->arc_state == ARC_CHAIN_FAIL) + if (msg->arc_cstate == ARC_CHAIN_FAIL) { return ARC_STAT_OK; } @@ -3202,55 +3197,71 @@ arc_eom(ARC_MESSAGE *msg) if (msg->arc_nsets == 0) { msg->arc_cstate = ARC_CHAIN_NONE; + return ARC_STAT_OK; } - else if (msg->arc_cstate != ARC_CHAIN_FAIL) + + /* validate the final ARC-Message-Signature */ + status = arc_validate_msg(msg, msg->arc_nsets); + if (status == ARC_STAT_INTERNAL) + { + return status; + } + if (status != ARC_STAT_OK) { - /* validate the final ARC-Message-Signature */ - if (arc_validate_msg(msg, msg->arc_nsets) != ARC_STAT_OK) + msg->arc_cstate = ARC_CHAIN_FAIL; + return ARC_STAT_OK; + } + + /* determine the oldest-pass value */ + for (int i = msg->arc_nsets - 1; i > 0; i--) + { + if (arc_validate_msg(msg, i) != ARC_STAT_OK) { - msg->arc_cstate = ARC_CHAIN_FAIL; + msg->arc_oldest_pass = i + 1; + break; } - else + if (i == 1) { - unsigned int set; - char *inst; - char *cv; - ARC_KVSET *kvset; - - msg->arc_cstate = ARC_CHAIN_PASS; - for (set = msg->arc_nsets; set > 0; set--) - { - for (kvset = arc_set_first(msg, ARC_KVSETTYPE_SEAL); - kvset != NULL; - kvset = arc_set_next(kvset, ARC_KVSETTYPE_SEAL)) - { - inst = arc_param_get(kvset, "i"); - if (atoi(inst) == set) - { - break; - } - } + /* everything passed */ + msg->arc_oldest_pass = 0; + } + } - cv = arc_param_get(kvset, "cv"); - if (!((set == 1 && strcasecmp(cv, "none") == 0) || - (set != 1 && strcasecmp(cv, "pass") == 0))) - { - /* the chain has already failed */ - msg->arc_cstate = ARC_CHAIN_FAIL; + /* validate each ARC-Seal */ + msg->arc_cstate = ARC_CHAIN_PASS; + for (int i = msg->arc_nsets; i > 0; i--) + { + char *cv; + ARC_KVSET *kvset; - /* note that it failed inbound */ - msg->arc_infail = true; + for (kvset = arc_set_first(msg, ARC_KVSETTYPE_SEAL); kvset != NULL; + kvset = arc_set_next(kvset, ARC_KVSETTYPE_SEAL)) + { + if (atoi(arc_param_get(kvset, "i")) == i) + { + break; + } + } - break; - } + cv = arc_param_get(kvset, "cv"); + if (!((i == 1 && strcasecmp(cv, "none") == 0) || + (i != 1 && strcasecmp(cv, "pass") == 0))) + { + /* the chain has already failed */ + msg->arc_cstate = ARC_CHAIN_FAIL; + msg->arc_infail = true; + return ARC_STAT_OK; + } - if (msg->arc_cstate != ARC_CHAIN_FAIL && - arc_validate_seal(msg, set) != ARC_STAT_OK) - { - msg->arc_cstate = ARC_CHAIN_FAIL; - break; - } - } + status = arc_validate_seal(msg, i); + if (status == ARC_STAT_INTERNAL) + { + return status; + } + if (status != ARC_STAT_OK) + { + msg->arc_cstate = ARC_CHAIN_FAIL; + return ARC_STAT_OK; } } @@ -3284,6 +3295,11 @@ arc_set_cv(ARC_MESSAGE *msg, ARC_CHAIN cv) /* only update the state if it's not a hard failure */ if (!msg->arc_infail) { + if (msg->arc_cstate != cv) + { + /* there's no way of knowing. */ + msg->arc_oldest_pass = -1; + } msg->arc_cstate = cv; } } @@ -3941,3 +3957,24 @@ arc_chain_custody_str(ARC_MESSAGE *msg, unsigned char *buf, size_t buflen) return appendlen; } + +/* +** ARC_CHAIN_OLDEST_PASS -- retrieve the oldest-pass value +** +** Parameters: +** msg -- ARC_MESSAGE object +** +** Return value: +** The lowest instance value where the AMS signature passed verification, +** `0` if all signatures passed, or `-1` for unknown. +*/ + +int +arc_chain_oldest_pass(ARC_MESSAGE *msg) +{ + if (msg->arc_cstate == ARC_CHAIN_PASS) + { + return msg->arc_oldest_pass; + } + return -1; +} diff --git a/libopenarc/arc.h b/libopenarc/arc.h index 189811c..42bae00 100644 --- a/libopenarc/arc.h +++ b/libopenarc/arc.h @@ -609,6 +609,12 @@ extern int arc_chain_custody_str(ARC_MESSAGE *msg, unsigned char *buf, size_t buflen); +/* +** ARC_CHAIN_OLDEST_PASS -- retrieve the oldest-pass value +*/ + +extern int arc_chain_oldest_pass(ARC_MESSAGE *); + /* ** ARC_MAIL_PARSE -- extract the local-part and domain-name from a structured ** header field diff --git a/openarc/openarc.c b/openarc/openarc.c index ebcfdb7..0ad4de4 100644 --- a/openarc/openarc.c +++ b/openarc/openarc.c @@ -3671,6 +3671,28 @@ reconcile_arc_state(msgctx afc, struct result *r) return initial_cv != new_cv; } +/* helper function to generate the arc authentication result for AR and AAR */ +static void +add_arc_authres(msgctx afc, struct arcf_config *conf, const char *ip) +{ + arc_dstring_printf(afc->mctx_tmpstr, "arc=%s", + arc_chain_status_str(afc->mctx_arcmsg)); + + if (arc_chain_oldest_pass(afc->mctx_arcmsg) >= 0) + { + arc_dstring_printf(afc->mctx_tmpstr, " header.oldest-pass=%d", + arc_chain_oldest_pass(afc->mctx_arcmsg)); + } + + if (conf->conf_authresip && ip[0] != '\0') + { + bool quote = !ares_istoken(ip); + + arc_dstring_printf(afc->mctx_tmpstr, " smtp.remote-ip=%s%s%s", + quote ? "\"" : "", ip, quote ? "\"" : ""); + } +} + /* ** MLFI_EOM -- handler called at the end of the message; we can now decide ** based on the configuration if and how to add the text @@ -3860,21 +3882,11 @@ mlfi_eom(SMFICTX *ctx) if (!arfound) { - /* Record the ARC status */ if (arc_dstring_len(afc->mctx_tmpstr) > 0) { arc_dstring_cat(afc->mctx_tmpstr, ";\n\t"); } - - arc_dstring_printf(afc->mctx_tmpstr, "arc=%s", - arc_chain_status_str(afc->mctx_arcmsg)); - - if (conf->conf_authresip && ipbuf[0] != '\0') - { - _Bool quote = !ares_istoken(ipbuf); - arc_dstring_printf(afc->mctx_tmpstr, " smtp.remote-ip=%s%s%s", - quote ? "\"" : "", ipbuf, quote ? "\"" : ""); - } + add_arc_authres(afc, conf, ipbuf); } /* @@ -3952,17 +3964,11 @@ mlfi_eom(SMFICTX *ctx) } arc_dstring_blank(afc->mctx_tmpstr); - arc_dstring_printf(afc->mctx_tmpstr, "%s%s; arc=%s", - cc->cctx_noleadspc ? " " : "", conf->conf_authservid, - arc_chain_status_str(afc->mctx_arcmsg)); - - if (conf->conf_authresip && ipbuf[0] != '\0') - { - _Bool quote = !ares_istoken(ipbuf); + arc_dstring_printf(afc->mctx_tmpstr, "%s%s; ", + cc->cctx_noleadspc ? " " : "", + conf->conf_authservid); - arc_dstring_printf(afc->mctx_tmpstr, " smtp.remote-ip=%s%s%s", - quote ? "\"" : "", ipbuf, quote ? "\"" : ""); - } + add_arc_authres(afc, conf, ipbuf); if (conf->conf_finalreceiver && arcchainlen > 0) { diff --git a/test/test_milter.py b/test/test_milter.py index 0fa0862..058cd89 100644 --- a/test/test_milter.py +++ b/test/test_milter.py @@ -122,11 +122,11 @@ def test_milter_staticmsg(run_miltertest): ['To', ' testrcpt@example.com'], ] res = run_miltertest(headers, False, 'test message\r\n') - assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] assert res['headers'][1][0] == 'ARC-Seal' assert 'cv=pass' in res['headers'][1][1] assert res['headers'][2][0] == 'ARC-Message-Signature' - assert res['headers'][3] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][3] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] def test_milter_canon_simple(run_miltertest): @@ -136,7 +136,7 @@ def test_milter_canon_simple(run_miltertest): res = run_miltertest(res['headers']) assert 'cv=pass' in res['headers'][0][1] - assert res['headers'][2] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][2] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] def test_milter_resign(run_miltertest): @@ -148,10 +148,10 @@ def test_milter_resign(run_miltertest): headers = [*res['headers'], *headers] res = run_miltertest(headers) - assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] if i <= 50: - assert res['headers'][3] == ['ARC-Authentication-Results', f' i={i}; example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][3] == ['ARC-Authentication-Results', f' i={i}; example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] assert 'cv=pass' in res['headers'][1][1] # quick and dirty parsing @@ -174,7 +174,7 @@ def test_milter_mode_s(run_miltertest): res = run_miltertest(res['headers']) assert len(res['headers']) == 3 assert 'cv=pass' in res['headers'][0][1] - assert res['headers'][2] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][2] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] def test_milter_mode_v(run_miltertest): @@ -440,9 +440,9 @@ def test_milter_ar_override_disabled(run_miltertest): res = run_miltertest(headers) - assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] assert 'cv=pass' in res['headers'][1][1] - assert res['headers'][3] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][3] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] def test_milter_ar_override_multi(run_miltertest): @@ -456,7 +456,7 @@ def test_milter_ar_override_multi(run_miltertest): ] res = run_miltertest(headers) - assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][0] == ['Authentication-Results', ' example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] assert 'cv=pass' in res['headers'][1][1] assert res['headers'][3] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass'] @@ -490,7 +490,26 @@ def test_milter_idna(run_miltertest): res = run_miltertest(res['headers']) assert 'cv=pass' in res['headers'][0][1] - assert res['headers'][2] == ['ARC-Authentication-Results', ' i=2; 시험.example.com; arc=pass smtp.remote-ip=127.0.0.1'] + assert res['headers'][2] == ['ARC-Authentication-Results', ' i=2; 시험.example.com; arc=pass header.oldest-pass=0 smtp.remote-ip=127.0.0.1'] + + +def test_milter_oldest_pass(run_miltertest): + """oldest-pass points at the most recent message modification""" + res = run_miltertest() + + headers = res['headers'] + res = run_miltertest(headers, body='second test body\r\n') + + # This doesn't have an oldest-pass because verification failed and was + # overridden by A-R. In this situation we could try to parse it from A-R, + # but currently that is not done. + assert res['headers'][3] == ['ARC-Authentication-Results', ' i=2; example.com; arc=pass smtp.remote-ip=127.0.0.1'] + + headers = [x for x in res['headers'] + headers if x[0] != 'Authentication-Results'] + + res = run_miltertest(headers, body='second test body\r\n') + + assert res['headers'][3] == ['ARC-Authentication-Results', ' i=3; example.com; arc=pass header.oldest-pass=2 smtp.remote-ip=127.0.0.1'] def test_milter_authresip(run_miltertest):