From: Jeremy Harris Date: Fri, 6 Sep 2024 11:29:23 +0000 (+0100) Subject: arc dynamic module X-Git-Url: https://git.exim.org/exim.git/commitdiff_plain/9b604221c5e94f8146f48e47a76865c11eedb7a1?ds=inline arc dynamic module --- diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 1189ce3f3..5220408e8 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -14,9 +14,9 @@ Version 4.98 3. Events smtp:fail:protocol and smtp:fail:syntax - 4. JSON and LDAP lookup support, SPF, DKIM and DMARC support, all the router - and authenticator drivers, and all the transport drivers except smtp, can - now be built as loadable modules + 4. JSON and LDAP lookup support, SPF, DKIM, DMARC and ARC support, all the + router and authenticator drivers, and all the transport drivers except + smtp, can now be built as loadable modules Version 4.98 ------------ diff --git a/doc/doc-txt/experimental-spec.txt b/doc/doc-txt/experimental-spec.txt index 56ee10f82..a73007700 100644 --- a/doc/doc-txt/experimental-spec.txt +++ b/doc/doc-txt/experimental-spec.txt @@ -498,6 +498,8 @@ Enable using EXPERIMENTAL_ARC=yes in your Local/Makefile. You must also have DKIM present (not disabled), and you very likely want to have SPF enabled. +It is possible to build as a dynamic-load module: set also SUPPORT_ARC=2. + Verification -- diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base index 12319967e..857c44776 100644 --- a/src/OS/Makefile-Base +++ b/src/OS/Makefile-Base @@ -495,8 +495,7 @@ transport-filter.pl: config ../src/transport-filter.src # are thrown away by the linker. OBJ_WITH_CONTENT_SCAN = malware.o mime.o regex.o spam.o spool_mbox.o -OBJ_EXPERIMENTAL = arc.o \ - bmi_spam.o \ +OBJ_EXPERIMENTAL = bmi_spam.o \ dane.o \ dcc.o \ imap_utf7.o \ @@ -685,6 +684,7 @@ HDRS = blob.h \ hintsdb/hints_tdb.h \ local_scan.h \ macros.h \ + miscmods/arc_api.h \ miscmods/dkim_api.h \ miscmods/dmarc_api.h \ miscmods/spf_api.h \ @@ -707,6 +707,7 @@ PHDRS = ../config.h \ ../hintsdb/hints_tdb.h \ ../local_scan.h \ ../macros.h \ + ../miscmods/arc_api.h \ ../miscmods/dkim_api.h \ ../miscmods/dmarc_api.h \ ../miscmods/spf_api.h \ @@ -900,7 +901,6 @@ spool_mbox.o: $(HDRS) spool_mbox.c # Dependencies for EXPERIMENTAL_* modules -arc.o: $(HDRS) miscmods/pdkim.h arc.c bmi_spam.o: $(HDRS) bmi_spam.c dane.o: $(HDRS) dane.c dane-openssl.c dcc.o: $(HDRS) dcc.h dcc.c diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile index 12f0ddd9c..1eb79a291 100755 --- a/src/scripts/Configure-Makefile +++ b/src/scripts/Configure-Makefile @@ -311,7 +311,7 @@ done <<-END routers ROUTER ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT transports TRANSPORT APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP auths AUTH CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS - miscmods SUPPORT _DKIM DMARC SPF + miscmods SUPPORT ARC _DKIM DMARC SPF END # See if there is a definition of EXIM_PERL in what we have built so far. diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks index a6521a95e..481f36fe3 100755 --- a/src/scripts/MakeLinks +++ b/src/scripts/MakeLinks @@ -97,6 +97,7 @@ mkdir $d cd $d # Makefile is generated for f in dummy.c \ + arc.c arc_api.h \ dkim.c dkim_transport.c dkim.h dkim_api.h \ pdkim/crypt_ver.h pdkim/pdkim.c pdkim/pdkim.h \ pdkim/pdkim_hash.h pdkim/signing.c pdkim/signing.h \ @@ -149,7 +150,7 @@ do done # EXPERIMENTAL_* -for f in arc.c bmi_spam.c bmi_spam.h dcc.c dcc.h dane.c dane-openssl.c \ +for f in bmi_spam.c bmi_spam.h dcc.c dcc.h dane.c dane-openssl.c \ danessl.h imap_utf7.c utf8.c xclient.c do ln -s ../src/$f $f diff --git a/src/src/acl.c b/src/src/acl.c index 878278313..18d892ec5 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -207,9 +207,17 @@ static condition_def conditions[] = { [ACLC_DELAY] = { US"delay", ACD_EXP | ACD_MOD, FORBIDDEN(ACL_BIT_NOTQUIT) }, #ifndef DISABLE_DKIM - [ACLC_DKIM_SIGNER] = { US"dkim_signers", ACD_EXP, + [ACLC_DKIM_SIGNER] = { US"dkim_signers", +# if SUPPORT_DKIM==2 + ACD_LOAD | +# endif + ACD_EXP, PERMITTED(ACL_BIT_DKIM) }, - [ACLC_DKIM_STATUS] = { US"dkim_status", ACD_EXP, + [ACLC_DKIM_STATUS] = { US"dkim_status", +# if SUPPORT_DKIM==2 + ACD_LOAD | +# endif + ACD_EXP, PERMITTED(ACL_BIT_DKIM | ACL_BIT_DATA | ACL_BIT_MIME # ifndef DISABLE_PRDR | ACL_BIT_PRDR @@ -394,6 +402,7 @@ for (condition_def * c = conditions; c < conditions + nelem(conditions); c++) } #endif +/******************************************************************************/ #ifndef MACRO_PREDEF @@ -410,20 +419,31 @@ typedef struct condition_module { # if SUPPORT_SPF==2 static int spf_condx[] = { ACLC_SPF, ACLC_SPF_GUESS, -1 }; # endif +# if SUPPORT_DKIM==2 +static int dkim_condx[] = { ACLC_DKIM_SIGNER, ACLC_DKIM_STATUS, -1 }; +# endif # if SUPPORT_DMARC==2 static int dmarc_condx[] = { ACLC_DMARC_STATUS, -1 }; # endif +/* These are modules which can be loaded on seeing an ACL condition +during readconf, The "arc" module is handled by custom coding. */ + static condition_module condition_modules[] = { # if SUPPORT_SPF==2 {.mod_name = US"spf", .conditions = spf_condx}, # endif -# if SUPPORT_SPF==2 +# if SUPPORT_DKIM==2 + {.mod_name = US"dkim", .conditions = dkim_condx}, +# endif +# if SUPPORT_DMARC==2 {.mod_name = US"dmarc", .conditions = dmarc_condx}, # endif }; -# endif +# endif /*LOOKUP_MODULE_DIR*/ + +/****************************/ /* Return values from decode_control() */ @@ -933,7 +953,7 @@ while ((s = (*func)())) if ((v = acl_checkname(name, verbs, nelem(verbs))) < 0) { - if (!this) + if (!this) /* not handling a verb right now */ { *error = string_sprintf("unknown ACL verb \"%s\" in \"%s\"", name, saveline); @@ -1002,6 +1022,9 @@ while ((s = (*func)())) condition_module * cm; uschar * s = NULL; + /* Over the list of modules we support, check the list of ACL conditions + each supports. This assumes no duplicates. */ + for (cm = condition_modules; cm < condition_modules + nelem(condition_modules); cm++) for (const int * cond = cm->conditions; *cond != -1; cond++) @@ -1022,7 +1045,21 @@ while ((s = (*func)())) return NULL; } } -#endif +# ifdef EXPERIMENTAL_ARC + else if (c == ACLC_VERIFY) /* Special handling for verify=arc; */ + { /* not invented a more general method yet- flag in verify_type_list? */ + const uschar * t = s; + uschar * e; + if ( *t++ == '=' && Uskip_whitespace(&t) && Ustrncmp(t, "arc", 3) == 0 + && !misc_mod_find(US"arc", &e)) + { + *error = string_sprintf("ACL error: failed to find module for '%s': %s", + conditions[c].name, e); + return NULL; + } + } +# endif +#endif /*LOOKUP_MODULE_DIR*/ cond = store_get(sizeof(acl_condition_block), GET_UNTAINTED); cond->next = NULL; @@ -1876,19 +1913,11 @@ switch(vp->value) #ifdef EXPERIMENTAL_ARC case VERIFY_ARC: - { /* Do Authenticated Received Chain checks in a separate function. */ - const uschar * condlist = CUS string_nextinlist(&list, &sep, NULL, 0); - int csep = 0; - uschar * cond; - - if (!(arc_state = acl_verify_arc())) return DEFER; - DEBUG(D_acl) debug_printf_indent("ARC verify result %s %s%s%s\n", arc_state, - arc_state_reason ? "(":"", arc_state_reason, arc_state_reason ? ")":""); - - if (!condlist) condlist = US"none:pass"; - while ((cond = string_nextinlist(&condlist, &csep, NULL, 0))) - if (Ustrcmp(arc_state, cond) == 0) return OK; - return FAIL; + { + const misc_module_info * mi = misc_mod_findonly(US"arc"); + typedef int (*fn_t)(const uschar *); + if (mi) return (((fn_t *) mi->functions)[ARC_VERIFY]) + (CUS string_nextinlist(&list, &sep, NULL, 0)); } #endif diff --git a/src/src/arc.c b/src/src/arc.c deleted file mode 100644 index a065ca8e3..000000000 --- a/src/src/arc.c +++ /dev/null @@ -1,2126 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ -/* Experimental ARC support for Exim - Copyright (c) The Exim Maintainers 2021 - 2024 - Copyright (c) Jeremy Harris 2018 - 2020 - License: GPL - SPDX-License-Identifier: GPL-2.0-or-later -*/ - -#include "exim.h" -#if defined EXPERIMENTAL_ARC -# if defined DISABLE_DKIM -# error DKIM must also be enabled for ARC -# else - -# include "functions.h" -# include "miscmods/pdkim.h" -# include "miscmods/signing.h" - -# ifdef SUPPORT_DMARC -# include "miscmods/dmarc.h" -# endif - -#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 { - struct hdr_rlist * prev; - BOOL used; - header_line * h; -} hdr_rlist; - -typedef struct arc_line { - header_line * complete; /* including the header name; nul-term */ - uschar * relaxed; - - /* identified tag contents */ - /*XXX t= for AS? */ - blob i; - blob cv; - blob a; - blob b; - blob bh; - blob d; - blob h; - blob s; - blob c; - blob l; - blob ip; - - /* tag content sub-portions */ - blob a_algo; - blob a_hash; - - blob c_head; - blob c_body; - - /* modified copy of b= field in line */ - blob rawsig_no_b_val; -} arc_line; - -typedef struct arc_set { - struct arc_set * next; - struct arc_set * prev; - - unsigned instance; - arc_line * hdr_aar; - arc_line * hdr_ams; - arc_line * hdr_as; - - const uschar * ams_verify_done; - BOOL ams_verify_passed; -} arc_set; - -typedef struct arc_ctx { - arc_set * arcset_chain; - arc_set * arcset_chain_last; -} arc_ctx; - -#define ARC_HDR_AAR US"ARC-Authentication-Results:" -#define ARC_HDRLEN_AAR 27 -#define ARC_HDR_AMS US"ARC-Message-Signature:" -#define ARC_HDRLEN_AMS 22 -#define ARC_HDR_AS US"ARC-Seal:" -#define ARC_HDRLEN_AS 9 -#define HDR_AR US"Authentication-Results:" -#define HDRLEN_AR 23 - -typedef enum line_extract { - le_instance_only, - le_instance_plus_ip, - le_all -} line_extract_t; - -static misc_module_info * arc_dkim_mod_info; - -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 }; - -/* We build a context for either Sign or Verify. - -For Verify, it's a fresh new one for ACL verify=arc - there is no connection -with the single line handling done during reception via the DKIM feed. - -For Verify we do it twice; initially during reception (via the DKIM feed) -and then later for the full verification. - -The former only looks at AMS headers, to discover what hash(es) we need done for -ARC on the message body; we call back to the DKIM code to set up so that it does -them for us during reception. That call needs info from many of the AMS tags; -arc_parse_line() for only the AMS is called asking for all the tag types. -That context is then discarded. - -Later, for Verify, we look at ARC headers again and then grab the hash result -from the DKIM layer. arc_parse_line() is called for all 3 line types, -gathering info for only 'i' and 'ip' tags from AAR headers, -for all tag types from AMS and AS headers. - - -For Sign, while running through the existing headers (before adding any for -this signing operation, we "take copies" of the headers, we call -arc_parse_line() gathering only the 'i' tag (instance) information. -*/ - - -/******************************************************************************/ - -/* We need a module init function, to check on the dkim module being present -(and we may as well stack it's modinfo ptr) - -For now (until we do an arc module), called from exim.c main(). -*/ -BOOL -arc_init(void) -{ -uschar * errstr = NULL; -if ((arc_dkim_mod_info = misc_mod_find(US"dkim", &errstr))) - return TRUE; -log_write(0, LOG_MAIN|LOG_PANIC, "arc: %s", errstr); -return FALSE; -} - -/******************************************************************************/ - - -/* Get the instance number from the header. -Return 0 on error */ -static unsigned -arc_instance_from_hdr(const arc_line * al) -{ -const uschar * s = al->i.data; -if (!s || !al->i.len) return 0; -return (unsigned) atoi(CCS s); -} - - -static uschar * -skip_fws(uschar * s) -{ -uschar c = *s; -while (c && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) c = *++s; -return s; -} - - -/* Locate instance struct on chain, inserting a new one if -needed. The chain is in increasing-instance-number order -by the "next" link, and we have a "prev" link also. -*/ - -static arc_set * -arc_find_set(arc_ctx * ctx, unsigned i) -{ -arc_set ** pas, * as, * next, * prev; - -for (pas = &ctx->arcset_chain, prev = NULL, next = ctx->arcset_chain; - as = *pas; pas = &as->next) - { - if (as->instance > i) break; - if (as->instance == i) - { - DEBUG(D_acl) debug_printf("ARC: existing instance %u\n", i); - return as; - } - next = as->next; - prev = as; - } - -DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i); -*pas = as = store_get(sizeof(arc_set), GET_UNTAINTED); -memset(as, 0, sizeof(arc_set)); -as->next = next; -as->prev = prev; -as->instance = i; -if (next) - next->prev = as; -else - ctx->arcset_chain_last = as; -return as; -} - - - -/* Insert a tag content into the line structure. -Note this is a reference to existing data, not a copy. -Check for already-seen tag. -The string-pointer is on the '=' for entry. Update it past the -content (to the ;) on return; -*/ - -static uschar * -arc_insert_tagvalue(arc_line * al, unsigned loff, uschar ** ss) -{ -uschar * s = *ss; -uschar c = *++s; -blob * b = (blob *)(US al + loff); -size_t len = 0; - -/* [FWS] tag-value [FWS] */ - -if (b->data) return US"fail"; -s = skip_fws(s); /* FWS */ - -b->data = s; -while ((c = *s) && c != ';') { len++; s++; } -*ss = s; -while (len && ((c = s[-1]) == ' ' || c == '\t' || c == '\n' || c == '\r')) - { s--; len--; } /* FWS */ -b->len = len; -return NULL; -} - - -/* Inspect a header line, noting known tag fields. -Check for duplicate named tags. - -See the file block comment for how this is used. - -Return: NULL for good, or an error string -*/ - -static uschar * -arc_parse_line(arc_line * al, header_line * h, unsigned off, line_extract_t l_ext) -{ -uschar * s = h->text + off; -uschar * r = NULL; -uschar c; - -al->complete = h; - -if (l_ext == le_all) /* need to grab rawsig_no_b */ - { - al->rawsig_no_b_val.data = store_get(h->slen + 1, GET_TAINTED); - memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */ - r = al->rawsig_no_b_val.data + off; - al->rawsig_no_b_val.len = off; - } - -/* tag-list = tag-spec *( ";" tag-spec ) [ ";" ] */ - -while ((c = *s)) - { - char tagchar; - uschar * t; - unsigned i = 0; - uschar * fieldstart = s; - uschar * bstart = NULL, * bend; - - /* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */ - /*X or just a naked FQDN, in a AAR ! */ - - s = skip_fws(s); /* leading FWS */ - if (!*s) break; - tagchar = *s++; - if (!*(s = skip_fws(s))) break; /* FWS */ - - switch (tagchar) - { - case 'a': /* a= AMS algorithm */ - if (l_ext == le_all && *s == '=') - { - if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup"; - - /* substructure: algo-hash (eg. rsa-sha256) */ - - t = al->a_algo.data = al->a.data; - while (*t != '-') - if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value"; - al->a_algo.len = i; - if (*t++ != '-') return US"no '-' in 'a' value"; - al->a_hash.data = t; - al->a_hash.len = al->a.len - i - 1; - } - break; - case 'b': - if (l_ext == le_all) - { - gstring * g = NULL; - - switch (*s) - { - case '=': /* b= AMS signature */ - if (al->b.data) return US"already b data"; - bstart = s+1; - - /* The signature can have FWS inserted in the content; - make a stripped copy */ - - while ((c = *++s) && c != ';') - if (c != ' ' && c != '\t' && c != '\n' && c != '\r') - g = string_catn(g, s, 1); - if (!g) return US"no b= value"; - al->b.len = len_string_from_gstring(g, &al->b.data); - gstring_release_unused(g); - bend = s; - break; - case 'h': /* bh= AMS body hash */ - s = skip_fws(++s); /* FWS */ - if (*s == '=') - { - if (al->bh.data) return US"already bh data"; - - /* The bodyhash can have FWS inserted in the content; - make a stripped copy */ - - while ((c = *++s) && c != ';') - if (c != ' ' && c != '\t' && c != '\n' && c != '\r') - g = string_catn(g, s, 1); - if (!g) return US"no bh= value"; - al->bh.len = len_string_from_gstring(g, &al->bh.data); - gstring_release_unused(g); - } - break; - default: - return US"b? tag"; - } - } - break; - case 'c': - if (l_ext == le_all) switch (*s) - { - case '=': /* c= AMS canonicalisation */ - if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup"; - - /* substructure: head/body (eg. relaxed/simple)) */ - - t = al->c_head.data = al->c.data; - while (isalpha(*t)) - if (!*t++ || ++i > al->a.len) break; - al->c_head.len = i; - if (*t++ == '/') /* /body is optional */ - { - al->c_body.data = t; - al->c_body.len = al->c.len - i - 1; - } - else - { - al->c_body.data = US"simple"; - al->c_body.len = 6; - } - break; - case 'v': /* cv= AS validity */ - s = skip_fws(s); - if (*++s == '=') - if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) - return US"cv tag dup"; - break; - } - break; - case 'd': /* d= AMS domain */ - if (l_ext == le_all && *s == '=') - if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) - return US"d tag dup"; - break; - case 'h': /* h= AMS headers */ - if (*s == '=') - if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) - return US"h tag dup"; - break; - case 'i': /* i= ARC set instance */ - if (*s == '=') - { - if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) - return US"i tag dup"; - if (l_ext == le_instance_only) - goto done; /* early-out */ - } - break; - case 'l': /* l= bodylength */ - if (l_ext == le_all && *s == '=') - if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) - return US"l tag dup"; - break; - case 's': - if (*s == '=' && l_ext == le_all) - { - if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) - return US"s tag dup"; - } - else if ( l_ext == le_instance_plus_ip - && Ustrncmp(s, "mtp.remote-ip", 13) == 0) - { /* smtp.remote-ip= AAR reception data */ - s += 13; - s = skip_fws(s); - if (*s != '=') return US"smtp.remote_ip tag val"; - if (arc_insert_tagvalue(al, offsetof(arc_line, ip), &s)) - return US"ip tag dup"; - } - break; - } - - while ((c = *s) && c != ';') s++; /* end of this tag=value */ - if (c) s++; /* ; after tag-spec */ - - /* for all but the b= tag, copy the field including FWS. For the b=, - drop the tag content. */ - - if (r) - if (bstart) - { - size_t n = bstart - fieldstart; - memcpy(r, fieldstart, n); /* FWS "b=" */ - r += n; - al->rawsig_no_b_val.len += n; - n = s - bend; - memcpy(r, bend, n); /* FWS ";" */ - r += n; - al->rawsig_no_b_val.len += n; - } - else - { - size_t n = s - fieldstart; - memcpy(r, fieldstart, n); - r += n; - al->rawsig_no_b_val.len += n; - } - } - -if (r) - *r = '\0'; - -done: -/* debug_printf("%s: finshed\n", __FUNCTION__); */ -return NULL; -} - - -/* Insert one header line in the correct set of the chain, -adding instances as needed and checking for duplicate lines. -*/ - -static uschar * -arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff, - line_extract_t l_ext, arc_line ** alp_ret) -{ -unsigned i; -arc_set * as; -arc_line * al = store_get(sizeof(arc_line), GET_UNTAINTED), ** alp; -uschar * e; - -memset(al, 0, sizeof(arc_line)); - -if ((e = arc_parse_line(al, h, off, l_ext))) - { - DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e); - return string_sprintf("line parse: %s", e); - } -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"; - -*alp = al; -if (alp_ret) *alp_ret = al; -return NULL; -} - - - -/* Called for both Sign and Verify */ - -static const uschar * -arc_try_header(arc_ctx * ctx, header_line * h, BOOL is_signing) -{ -const uschar * e; - -/*debug_printf("consider hdr '%s'\n", h->text);*/ -if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0) - { - DEBUG(D_acl) - { - int len = h->slen; - uschar * s; - for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) - s--, len--; - debug_printf("ARC: found AAR: %.*s\n", len, h->text); - } - if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar), - is_signing ? le_instance_only : le_instance_plus_ip, NULL))) - { - DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e); - return string_sprintf("inserting AAR: %s", e); - } - } -else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0) - { - arc_line * ams; - - DEBUG(D_acl) - { - int len = h->slen; - uschar * s; - for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) - s--, len--; - debug_printf("ARC: found AMS: %.*s\n", len, h->text); - } - if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams), - is_signing ? le_instance_only : le_all, &ams))) - { - DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e); - return string_sprintf("inserting AMS: %s", e); - } - - /* defaults */ - if (!ams->c.data) - { - ams->c_head.data = US"simple"; ams->c_head.len = 6; - ams->c_body = ams->c_head; - } - } -else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0) - { - DEBUG(D_acl) - { - int len = h->slen; - uschar * s; - for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) - s--, len--; - debug_printf("ARC: found AS: %.*s\n", len, h->text); - } - if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as), - is_signing ? le_instance_only : le_all, NULL))) - { - DEBUG(D_acl) debug_printf("inserting AS: %s\n", e); - return string_sprintf("inserting AS: %s", e); - } - } -return NULL; -} - - - -/* Gather the chain of arc sets from the headers. -Check for duplicates while that is done. Also build the -reverse-order headers list. -Called on an ACL verify=arc condition. - -Return: ARC state if determined, eg. by lack of any ARC chain. -*/ - -static const uschar * -arc_vfy_collect_hdrs(arc_ctx * ctx) -{ -header_line * h; -hdr_rlist * r = NULL, * rprev = NULL; -const uschar * e; - -DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n"); -for (h = header_list; h; h = h->next) - { - r = store_get(sizeof(hdr_rlist), GET_UNTAINTED); - r->prev = rprev; - r->used = FALSE; - r->h = h; - rprev = r; - - if ((e = arc_try_header(ctx, h, FALSE))) - { - arc_state_reason = string_sprintf("collecting headers: %s", e); - return US"fail"; - } - } -headers_rlist = r; - -if (!ctx->arcset_chain) return US"none"; -return NULL; -} - - -static BOOL -arc_cv_match(arc_line * al, const uschar * s) -{ -return Ustrncmp(s, al->cv.data, al->cv.len) == 0; -} - -/******************************************************************************/ -/* Service routines provided by the dkim module */ - -static int -arc_dkim_hashname_blob_to_type(const blob * name) -{ -typedef int (*fn_t)(const blob *); -return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HASHNAME_TO_TYPE]) (name); -} -static hashmethod -arc_dkim_hashtype_to_method(int hashtype) -{ -typedef hashmethod (*fn_t)(int); -return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HASHTYPE_TO_METHOD]) (hashtype); -} -static hashmethod -arc_dkim_hashname_blob_to_method(const blob * name) -{ -typedef hashmethod (*fn_t)(const blob *); -return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HASHNAME_TO_METHOD]) (name); -} - -/******************************************************************************/ - -/* Do a "relaxed" canonicalization of a header */ -static uschar * -arc_relax_header_n(const uschar * text, int len, BOOL append_crlf) -{ -typedef uschar * (*fn_t)(const uschar *, int, BOOL); -return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HEADER_RELAX]) - (text, len, append_crlf); -} - - - -/* Return the hash of headers from the message that the AMS claims it -signed. -*/ - -static void -arc_get_verify_hhash(arc_ctx * ctx, arc_line * ams, blob * hhash) -{ -const uschar * headernames = string_copyn(ams->h.data, ams->h.len); -const uschar * hn; -int sep = ':'; -hdr_rlist * r; -BOOL relaxed = Ustrncmp(US"relaxed", ams->c_head.data, ams->c_head.len) == 0; -hashmethod hm = arc_dkim_hashname_blob_to_method(&ams->a_hash); -hctx hhash_ctx; -const uschar * s; -int len; - -if (hm < 0 || !exim_sha_init(&hhash_ctx, hm)) - { - DEBUG(D_acl) - debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); - return; - } - -/* For each headername in the list from the AMS (walking in order) -walk the message headers in reverse order, adding to the hash any -found for the first time. For that last point, maintain used-marks -on the list of message headers. */ - -DEBUG(D_acl) debug_printf("ARC: AMS header data for verification:\n"); - -for (r = headers_rlist; r; r = r->prev) - r->used = FALSE; -while ((hn = string_nextinlist(&headernames, &sep, NULL, 0))) - for (r = headers_rlist; r; r = r->prev) - if ( !r->used - && strncasecmp(CCS (s = r->h->text), CCS hn, Ustrlen(hn)) == 0 - ) - { - if (relaxed) s = arc_relax_header_n(s, r->h->slen, TRUE); - - DEBUG(D_acl) debug_printf("%Z\n", s); - exim_sha_update_string(&hhash_ctx, s); - r->used = TRUE; - break; - } - -/* 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 = arc_relax_header_n(s, len, FALSE)); -DEBUG(D_acl) debug_printf("%.*Z\n", len, s); -exim_sha_update(&hhash_ctx, s, len); - -exim_sha_finish(&hhash_ctx, hhash); -DEBUG(D_acl) - { debug_printf("ARC: header hash: %.*H\n", hhash->len, hhash->data); } -return; -} - - - - -static blob * -arc_line_to_pubkey(arc_line * al, const uschar ** errstr) -{ -typedef const uschar * (*fn_t)(const uschar *, blob **, const uschar **); -blob * pubkey; -const uschar * hashes; -const uschar * srvtype = - (((fn_t *) arc_dkim_mod_info->functions)[DKIM_DNS_PUBKEY]) - (string_sprintf("%.*s._domainkey.%.*s", - (int)al->s.len, al->s.data, (int)al->d.len, al->d.data), - &pubkey, &hashes); - -/*XXX do we need a blob-string printf %handler? Other types of blob? */ - -if (!srvtype) - { *errstr = US"pubkey dns lookup fail"; return NULL; } -if ((Ustrcmp(srvtype, "*") != 0 && Ustrcmp(srvtype, "email") != 0)) - { - *errstr = string_sprintf("pubkey format error: srvtype '%s'", srvtype); - return NULL; - } - -/* If the pubkey limits use to specified hashes, reject unusable -signatures. XXX should we have looked for multiple dns records? */ - -if (hashes) - { - const uschar * list = hashes, * ele; - int sep = ':'; - - while ((ele = string_nextinlist(&list, &sep, NULL, 0))) - if (Ustrncmp(ele, al->a_hash.data, al->a_hash.len) == 0) break; - if (!ele) - { - DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n", - hashes, (int)al->a.len, al->a.data); - *errstr = US"no usable sig for this pubkey hash list"; - return NULL; - } - } -return pubkey; -} - - - - -/* Set up a body hashing method on the given signature-context -(creates a new one if needed, or uses an already-present one). - -Arguments: - signing TRUE for signing, FALSE for verification - c canonicalization spec, text form - ah hash, text form - bodylen byte count for message body - -Return: pointer to hashing method struct -*/ - -static pdkim_bodyhash * -arc_set_bodyhash(BOOL signing, - const blob * c, const blob * ah, long bodylen) -{ -typedef pdkim_bodyhash * (*fn_t)(BOOL, - const blob * canon, const blob * hash, long bodylen); - -return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_SET_BODYHASH]) - (signing, c, ah, bodylen); -} - - - - -static pdkim_bodyhash * -arc_ams_setup_vfy_bodyhash(arc_line * ams) -{ -blob * c = &ams->c; -long bodylen = ams->l.data - ? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) - : -1; - -if (!c->data) - { - c->data = US"simple"; /* RFC 6376 (DKIM) default */ - c->len = 6; - } - -return arc_set_bodyhash(FALSE, c, &ams->a_hash, bodylen); -} - - - -static void -arc_decode_base64(const uschar * str, blob * b) -{ -int dlen = b64decode(str, &b->data, str); -if (dlen < 0) b->data = NULL; -b->len = dlen; -} - - - -static int -arc_sig_verify(arc_set * as, arc_line * al, hashmethod hm, - blob * hhash_computed, blob * sighash, - const uschar * why, const uschar ** errstr_p) -{ -blob * pubkey; -const uschar * errstr = NULL; -int rc; -typedef int (*fn_t) - (const blob *, const blob *, hashmethod, const blob *, const uschar **); - -/* Get the public key from DNS */ - -/*XXX dkim module */ -if (!(pubkey = arc_line_to_pubkey(al, &errstr))) - { - *errstr_p = string_sprintf("%s (%s)", errstr, why); - return ERROR; - } - -rc = (((fn_t *) arc_dkim_mod_info->functions)[DKIM_SIG_VERIFY]) - (sighash, hhash_computed, hm, pubkey, &errstr); -switch (rc) - { - case OK: - break; - case FAIL: - DEBUG(D_acl) - debug_printf("ARC i=%d %s verify %s\n", as->instance, why, errstr); - break; - case ERROR: - DEBUG(D_acl) debug_printf("ARC verify %s init: %s\n", why, errstr); - break; - } -return rc; -} - - - - -/* Verify an AMS. This is a DKIM-sig header, but with an ARC i= tag -and without a DKIM v= tag. -*/ - -static const uschar * -arc_ams_verify(arc_ctx * ctx, arc_set * as) -{ -arc_line * ams = as->hdr_ams; -pdkim_bodyhash * b; -blob sighash; -blob hhash_computed; -hashmethod hm; -const uschar * errstr; -int rc; - -as->ams_verify_done = US"in-progress"; - -/* Check the AMS has all the required tags: - "a=" algorithm - "b=" signature - "bh=" body hash - "d=" domain (for key lookup) - "h=" headers (included in signature) - "s=" key-selector (for key lookup) -*/ -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) - { - debug_printf("ARC i=%d AMS Body bytes hashed: %lu\n" - " Body %.*s computed: ", - as->instance, b->signed_body_bytes, - (int)ams->a_hash.len, ams->a_hash.data); - debug_printf("%.*H\n", b->bh.len, b->bh.data); - } - -/* We know the bh-tag blob is of a nul-term string, so safe as a string */ - -if ( !ams->bh.data - || (arc_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len) - || memcmp(sighash.data, b->bh.data, b->bh.len) != 0 - ) - { - DEBUG(D_acl) - { - debug_printf("ARC i=%d AMS Body hash from headers: ", as->instance); - debug_printf("%.*H\n", sighash.len, sighash.data); - debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance); - } - 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); - -/* We know the b-tag blob is of a nul-term string, so safe as a string */ -arc_decode_base64(ams->b.data, &sighash); - -arc_get_verify_hhash(ctx, ams, &hhash_computed); - -if ((hm = arc_dkim_hashname_blob_to_method(&ams->a_hash)) < 0) - { - DEBUG(D_acl) debug_printf("ARC i=%d AMS verify bad a_hash\n", as->instance); - return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify"; - } - -rc = arc_sig_verify(as, ams, hm, &hhash_computed, &sighash, US"AMS", &errstr); -if (rc != OK) - return as->ams_verify_done = arc_state_reason = - rc == FAIL ? US"AMS sig nonverify" : errstr; - -DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance); -as->ams_verify_passed = TRUE; -return NULL; -} - - - -/* Check the sets are instance-continuous and that all -members are present. Check that no arc_seals are "fail". -Set the highest instance number global. -Verify the latest AMS. -*/ -static uschar * -arc_headers_check(arc_ctx * ctx) -{ -arc_set * as; -int inst; -BOOL ams_fail_found = FALSE; - -if (!(as = ctx->arcset_chain_last)) - return US"none"; - -for(inst = as->instance; as; as = as->prev, inst--) - { - 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. */ - - if (!ams_fail_found) - if (arc_ams_verify(ctx, as)) - 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 = arc_received->instance; - -/* We can skip the latest-AMS validation, if we already did it. */ - -as = ctx->arcset_chain_last; -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; -} - - -/******************************************************************************/ -static const uschar * -arc_seal_verify(arc_ctx * ctx, arc_set * as) -{ -arc_line * hdr_as = as->hdr_as; -arc_set * as2; -hashmethod hm; -hctx hhash_ctx; -blob hhash_computed; -blob sighash; -const uschar * errstr; -int rc; - -DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance); -/* - 1. If the value of the "cv" tag on that seal is "fail", the - chain state is "fail" and the algorithm stops here. (This - step SHOULD be skipped if the earlier step (2.1) was - performed) [it was] - - 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv - == "none" && i != 1)) then the chain state is "fail" and the - algorithm stops here (note that the ordering of the logic is - structured for short-circuit evaluation). -*/ - -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 - the ARC-Seal. -*/ - -hm = arc_dkim_hashname_blob_to_method(&hdr_as->a_hash); - -if (hm < 0 || !exim_sha_init(&hhash_ctx, hm)) - { - DEBUG(D_acl) - debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); - arc_state_reason = US"seal hash setup error"; - return US"fail"; - } - -/* - 4. Compute the canonicalized form of the ARC header fields, in - the order described in Section 5.4.2, using the "relaxed" - 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"); -for (as2 = ctx->arcset_chain; - as2 && as2->instance <= as->instance; - as2 = as2->next) - { - arc_line * al; - uschar * s; - int len; - - al = as2->hdr_aar; - if (!(s = al->relaxed)) - /*XXX dkim module */ - al->relaxed = s = arc_relax_header_n(al->complete->text, - al->complete->slen, TRUE); - len = Ustrlen(s); - DEBUG(D_acl) debug_printf("%Z\n", s); - exim_sha_update(&hhash_ctx, s, len); - - al = as2->hdr_ams; - if (!(s = al->relaxed)) - /*XXX dkim module */ - al->relaxed = s = arc_relax_header_n(al->complete->text, - al->complete->slen, TRUE); - len = Ustrlen(s); - DEBUG(D_acl) debug_printf("%Z\n", s); - exim_sha_update(&hhash_ctx, s, len); - - al = as2->hdr_as; - if (as2->instance == as->instance) - /*XXX dkim module */ - s = arc_relax_header_n(al->rawsig_no_b_val.data, - al->rawsig_no_b_val.len, FALSE); - else if (!(s = al->relaxed)) - /*XXX dkim module */ - al->relaxed = s = arc_relax_header_n(al->complete->text, - al->complete->slen, TRUE); - len = Ustrlen(s); - DEBUG(D_acl) debug_printf("%Z\n", s); - exim_sha_update(&hhash_ctx, s, len); - } - -/* - 5. Retrieve the final digest from the hash function. -*/ - -exim_sha_finish(&hhash_ctx, &hhash_computed); -DEBUG(D_acl) - { - debug_printf("ARC i=%d AS Header %.*s computed: ", - as->instance, (int)hdr_as->a_hash.len, hdr_as->a_hash.data); - debug_printf("%.*H\n", hhash_computed.len, hhash_computed.data); - } - - -/* - 6. Retrieve the public key identified by the "s" and "d" tags in - the ARC-Seal, as described in Section 4.1.6. - -Done below, in arc_sig_verify(). - - 7. Determine whether the signature portion ("b" tag) of the ARC- - Seal and the digest computed above are valid according to the - public key. (See also Section Section 8.4 for failure case - handling) - - 8. If the signature is not valid, the chain state is "fail" and - the algorithm stops here. -*/ - -/* We know the b-tag blob is of a nul-term string, so safe as a string */ -arc_decode_base64(hdr_as->b.data, &sighash); - -rc = arc_sig_verify(as, hdr_as, hm, &hhash_computed, &sighash, US"AS", &errstr); -if (rc != OK) - { - if (rc == FAIL) arc_state_reason = US"seal sigverify error"; - return US"fail"; - } - -DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d pass\n", as->instance); -return NULL; -} - - -static const uschar * -arc_verify_seals(arc_ctx * ctx) -{ -arc_set * as = ctx->arcset_chain_last; - -if (!as) - return US"none"; - -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; -} -/******************************************************************************/ - -/* Do ARC verification. Called from DATA ACL, on a verify = arc -condition. No arguments; we are checking globals. - -Return: The ARC state, or NULL on error. -*/ - -const uschar * -acl_verify_arc(void) -{ -const uschar * res; - -memset(&arc_verify_ctx, 0, sizeof(arc_verify_ctx)); - -/* AS evaluation, per -https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6 -*/ -/* 1. Collect all ARC sets currently on the message. If there were - none, the ARC state is "none" and the algorithm stops here. -*/ - -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 - exactly one of each of the three ARC-specific header fields), - then the chain state is "fail" and the algorithm stops here. - - 1. To avoid the overhead of unnecessary computation and delay - from crypto and DNS operations, the cv value for all ARC- - Seal(s) MAY be checked at this point. If any of the values - are "fail", then the overall state of the chain is "fail" and - the algorithm stops here. - - 3. Conduct verification of the ARC-Message-Signature header field - bearing the highest instance number. If this verification fails, - then the chain state is "fail" and the algorithm stops here. -*/ - -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 - following logic: - - 1. If the value of the "cv" tag on that seal is "fail", the - chain state is "fail" and the algorithm stops here. (This - step SHOULD be skipped if the earlier step (2.1) was - performed) - - 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv - == "none" && i != 1)) then the chain state is "fail" and the - algorithm stops here (note that the ordering of the logic is - structured for short-circuit evaluation). - - 3. Initialize a hash function corresponding to the "a" tag of - the ARC-Seal. - - 4. Compute the canonicalized form of the ARC header fields, in - the order described in Section 5.4.2, using the "relaxed" - header canonicalization defined in Section 3.4.2 of - [RFC6376]. Pass the canonicalized result to the hash - function. - - 5. Retrieve the final digest from the hash function. - - 6. Retrieve the public key identified by the "s" and "d" tags in - the ARC-Seal, as described in Section 4.1.6. - - 7. Determine whether the signature portion ("b" tag) of the ARC- - Seal and the digest computed above are valid according to the - public key. (See also Section Section 8.4 for failure case - handling) - - 8. If the signature is not valid, the chain state is "fail" and - the algorithm stops here. - - 5. If all seals pass validation, then the chain state is "pass", and - the algorithm is complete. -*/ - -if ((res = arc_verify_seals(&arc_verify_ctx))) - goto out; - -res = US"pass"; - -out: - return res; -} - -/******************************************************************************/ - -/* Prepend the header to the rlist */ - -static hdr_rlist * -arc_rlist_entry(hdr_rlist * list, const uschar * s, int len) -{ -hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line), GET_UNTAINTED); -header_line * h = r->h = (header_line *)(r+1); - -r->prev = list; -r->used = FALSE; -h->next = NULL; -h->type = 0; -h->slen = len; -h->text = US s; - -return r; -} - - -/* Walk the given headers strings identifying each header, and construct -a reverse-order list. -*/ - -static hdr_rlist * -arc_sign_scan_headers(arc_ctx * ctx, gstring * sigheaders) -{ -const uschar * s; -hdr_rlist * rheaders = NULL; - -s = sigheaders ? sigheaders->s : NULL; -if (s) while (*s) - { - const uschar * s2 = s; - - /* This works for either NL or CRLF lines; also nul-termination */ - while (*++s2) - if (*s2 == '\n' && s2[1] != '\t' && s2[1] != ' ') break; - s2++; /* move past end of line */ - - rheaders = arc_rlist_entry(rheaders, s, s2 - s); - s = s2; - } -return rheaders; -} - - - -/* Return the A-R content, without identity, with line-ending and -NUL termination. */ - -static BOOL -arc_sign_find_ar(header_line * headers, const uschar * identity, blob * ret) -{ -header_line * h; -int ilen = Ustrlen(identity); - -ret->data = NULL; -for(h = headers; h; h = h->next) - { - uschar * s = h->text, c; - int len = h->slen; - - if (Ustrncmp(s, HDR_AR, HDRLEN_AR) != 0) continue; - s += HDRLEN_AR, len -= HDRLEN_AR; /* header name */ - while ( len > 0 - && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) - s++, len--; /* FWS */ - if (Ustrncmp(s, identity, ilen) != 0) continue; - s += ilen; len -= ilen; /* identity */ - if (len <= 0) continue; - if ((c = *s) && c == ';') s++, len--; /* identity terminator */ - while ( len > 0 - && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) - s++, len--; /* FWS */ - if (len <= 0) continue; - ret->data = s; - ret->len = len; - return TRUE; - } -return FALSE; -} - - - -/* Append a constructed AAR including CRLF. Add it to the arc_ctx too. */ - -static gstring * -arc_sign_append_aar(gstring * g, arc_ctx * ctx, - const uschar * identity, int instance, blob * ar) -{ -int aar_off = gstring_length(g); -arc_set * as = - store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line), GET_UNTAINTED); -arc_line * al = (arc_line *)(as+1); -header_line * h = (header_line *)(al+1); - -g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR); -g = string_fmt_append(g, " i=%d; %s; smtp.remote-ip=%s;\r\n\t", - instance, identity, sender_host_address); -g = string_catn(g, US ar->data, ar->len); - -h->slen = g->ptr - aar_off; -h->text = g->s + aar_off; -al->complete = h; -as->next = NULL; -as->prev = ctx->arcset_chain_last; -as->instance = instance; -as->hdr_aar = al; -if (instance == 1) - ctx->arcset_chain = as; -else - ctx->arcset_chain_last->next = as; -ctx->arcset_chain_last = as; - -DEBUG(D_transport) debug_printf("ARC: AAR '%.*s'\n", h->slen - 2, h->text); -return g; -} - - - -static BOOL -arc_sig_from_pseudoheader(gstring * hdata, int hashtype, const uschar * privkey, - blob * sig, const uschar * why) -{ -hashmethod hm = /*sig->keytype == KEYTYPE_ED25519*/ FALSE - ? HASH_SHA2_512 - : arc_dkim_hashtype_to_method(hashtype); - -blob hhash; -const uschar * errstr; -typedef const uschar * (*fn_t) - (const blob *, hashmethod, const uschar *, blob *); - -DEBUG(D_transport) - { - hctx hhash_ctx; - debug_printf("ARC: %s header data for signing:\n", why); - debug_printf("%.*Z\n", hdata->ptr, hdata->s); - - (void) exim_sha_init(&hhash_ctx, hm); - exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); - exim_sha_finish(&hhash_ctx, &hhash); - debug_printf("ARC: header hash: %.*H\n", hhash.len, hhash.data); - } - -if (FALSE /*need hash for Ed25519 or GCrypt signing*/ ) - { - hctx hhash_ctx; - (void) exim_sha_init(&hhash_ctx, arc_dkim_hashtype_to_method(hashtype)); - exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); - exim_sha_finish(&hhash_ctx, &hhash); - } -else - { - hhash.data = hdata->s; - hhash.len = hdata->ptr; - } - -errstr = (((fn_t *) arc_dkim_mod_info->functions)[DKIM_SIGN_DATA]) - (&hhash, hm, privkey, sig); -if (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; -} - - - -static gstring * -arc_sign_append_sig(gstring * g, blob * sig) -{ -/*debug_printf("%s: raw sig %.*H\n", __FUNCTION__, sig->len, sig->data);*/ -sig->data = b64encode(sig->data, sig->len); -sig->len = Ustrlen(sig->data); -for (;;) - { - int len = MIN(sig->len, 74); - g = string_catn(g, sig->data, len); - if ((sig->len -= len) == 0) break; - sig->data += len; - g = string_catn(g, US"\r\n\t ", 5); - } -g = string_catn(g, US";\r\n", 3); -gstring_release_unused(g); -string_from_gstring(g); -return g; -} - - -/* Append a constructed AMS including CRLF. Add it to the arc_ctx too. */ - -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, unsigned options) -{ -uschar * s; -gstring * hdata = NULL; -int col; -const blob ams_h = {.data = US"sha256", .len = 6}; /*XXX hardwired */ -int hashtype = arc_dkim_hashname_blob_to_type(&ams_h); -blob sig; -int ams_off; -arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED); -header_line * h = (header_line *)(al+1); - -/* debug_printf("%s\n", __FUNCTION__); */ - -/* Construct the to-be-signed AMS pseudo-header: everything but the sig. */ - -ams_off = gstring_length(g); -g = string_fmt_append(g, "%s i=%d; a=rsa-sha256; c=relaxed; d=%s; s=%s", - ARC_HDR_AMS, instance, identity, selector); /*XXX hardwired a= */ -if (options & ARC_SIGN_OPT_TSTAMP) - g = string_fmt_append(g, "; t=%lu", (u_long)now); -if (options & ARC_SIGN_OPT_EXPIRE) - g = string_fmt_append(g, "; x=%lu", (u_long)expire); -g = string_fmt_append(g, ";\r\n\tbh=%s;\r\n\th=", - b64encode(bodyhash->data, bodyhash->len)); - -for(col = 3; rheaders; rheaders = rheaders->prev) - { - const uschar * hnames = US"DKIM-Signature:" PDKIM_DEFAULT_SIGN_HEADERS; - uschar * name, * htext = rheaders->h->text; - int sep = ':'; - - /* Spot headers of interest */ - - while ((name = string_nextinlist(&hnames, &sep, NULL, 0))) - { - int len = Ustrlen(name); - if (strncasecmp(CCS htext, CCS name, len) == 0) - { - /* If too long, fold line in h= field */ - - if (col + len > 78) g = string_catn(g, US"\r\n\t ", 5), col = 3; - - /* Add name to h= list */ - - g = string_catn(g, name, len); - g = string_catn(g, US":", 1); - col += len + 1; - - /* Accumulate header for hashing/signing */ - - hdata = string_cat(hdata, - /*XXX dkim module */ - arc_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */ - break; - } - } - } - -/* Lose the last colon from the h= list */ - -gstring_trim_trailing(g, ':'); - -g = string_catn(g, US";\r\n\tb=;", 7); - -/* Include the pseudo-header in the accumulation */ - -/*XXX dkim module */ -s = arc_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE); -hdata = string_cat(hdata, s); - -/* Calculate the signature from the accumulation */ -/*XXX does that need further relaxation? there are spaces embedded in the b= strings! */ - -if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AMS")) - return NULL; - -/* Lose the trailing semicolon from the psuedo-header, and append the signature -(folded over lines) and termination to complete it. */ - -gstring_trim(g, 1); -g = arc_sign_append_sig(g, &sig); - -h->slen = g->ptr - ams_off; -h->text = g->s + ams_off; -al->complete = h; -ctx->arcset_chain_last->hdr_ams = al; - -DEBUG(D_transport) debug_printf("ARC: AMS '%.*s'\n", h->slen - 2, h->text); -return g; -} - - - -/* Look for an arc= result in an A-R header blob. We know that its data -happens to be a NUL-term string. */ - -static uschar * -arc_ar_cv_status(blob * ar) -{ -const uschar * resinfo = ar->data; -int sep = ';'; -uschar * methodspec, * s; - -while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0))) - if (Ustrncmp(methodspec, US"arc=", 4) == 0) - { - uschar c; - for (s = methodspec += 4; - (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++; - return string_copyn(methodspec, s - methodspec); - } -return US"none"; -} - - - -/* Build the AS header and prepend it */ - -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, unsigned options) -{ -gstring * arcset; -uschar * status = arc_ar_cv_status(ar); -arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED); -header_line * h = (header_line *)(al+1); -uschar * badline_str; - -gstring * hdata = NULL; -const blob as_h = {.data = US"sha256", .len = 6}; /*XXX hardwired */ -int hashtype = arc_dkim_hashname_blob_to_type(&as_h); -blob sig; - -/* -- Generate AS - - no body coverage - - no h= tag; implicit coverage - - arc status from A-R - - if fail: - - coverage is just the new ARC set - including self (but with an empty b= in self) - - if non-fail: - - all ARC set headers, set-number order, aar then ams then as, - including self (but with an empty b= in self) -*/ -DEBUG(D_transport) debug_printf("ARC: building AS for status '%s'\n", status); - -/* Construct the AS except for the signature */ - -arcset = string_append(NULL, 9, - ARC_HDR_AS, - US" i=", string_sprintf("%d", instance), - US"; cv=", status, - US"; a=rsa-sha256; d=", identity, /*XXX hardwired */ - US"; s=", selector); /*XXX same as AMS */ -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; -h->text = arcset->s; -al->complete = h; -ctx->arcset_chain_last->hdr_as = al; - -/* For any but "fail" chain-verify status, walk the entire chain in order by -instance. For fail, only the new arc-set. Accumulate the elements walked. */ - -for (arc_set * as = Ustrcmp(status, US"fail") == 0 - ? ctx->arcset_chain_last : ctx->arcset_chain; - as; as = as->next) - { - arc_line * l; - /* Accumulate AAR then AMS then AS. Relaxed canonicalisation - is required per standard. */ - - badline_str = US"aar"; - if (!(l = as->hdr_aar)) goto badline; - h = l->complete; - /*XXX dkim module */ - hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, TRUE)); - badline_str = US"ams"; - if (!(l = as->hdr_ams)) goto badline; - h = l->complete; - /*XXX dkim module */ - hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, TRUE)); - badline_str = US"as"; - if (!(l = as->hdr_as)) goto badline; - h = l->complete; - /*XXX dkim module */ - hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, !!as->next)); - } - -/* Calculate the signature from the accumulation */ - -if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AS")) - return NULL; - -/* Lose the trailing semicolon */ -arcset->ptr--; -arcset = arc_sign_append_sig(arcset, &sig); -DEBUG(D_transport) debug_printf("ARC: AS '%.*s'\n", arcset->ptr - 2, arcset->s); - -/* Finally, append the AMS and AAR to the new AS */ - -return string_catn(arcset, arcset_interim->s, arcset_interim->ptr); - -badline: - DEBUG(D_transport) - debug_printf("ARC: while building AS, missing %s in chain\n", badline_str); - return NULL; -} - - -/**************************************/ - -/*XXX not static currently as the smtp tpt calls us */ -/* Really returns pdkim_bodyhash* - but there's an ordering -problem for functions.h so call it void* */ - -void * -arc_ams_setup_sign_bodyhash(void) -{ -blob canon = {.data = US"relaxed", .len = 7}; /*XXX hardwired */ -blob hash = {.data = US"sha256", .len = 6}; /*XXX hardwired */ - -DEBUG(D_transport) debug_printf("ARC: requesting bodyhash\n"); - -return arc_set_bodyhash(TRUE, &canon, &hash, -1); -} - - - -void -arc_sign_init(void) -{ -memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx)); -headers_rlist = NULL; -} - - - -/* 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. - -We have to take the feed from DKIM as, in the transport-filter case, the -headers are not in memory at the time of the call to arc_sign(). - -Take a copy of the header and construct a reverse-order list. -Also parse ARC-chain headers and build the chain struct, retaining pointers -into the copies. -*/ - -static const uschar * -arc_header_sign_feed(gstring * g) -{ -uschar * s = string_copy_from_gstring(g); -headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr); -return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE); -} - - - -/* Per RFCs 6376, 7489 the only allowed chars in either an ADMD id -or a selector are ALPHA/DIGGIT/'-'/'.' - -Check, to help catch misconfigurations such as a missing selector -element in the arc_sign list. -*/ - -static BOOL -arc_valid_id(const uschar * s) -{ -for (uschar c; c = *s++; ) - if (!isalnum(c) && c != '-' && c != '.') return FALSE; -return TRUE; -} - - - -/* ARC signing. Called from the smtp transport, if the arc_sign option is set. -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. - Optional fourth element: comma-sep list of options. - Already expanded - sigheaders Any signature headers already generated, eg. by DKIM, or NULL - errstr Error string - -Return value - Set of headers to prepend to the message, including the supplied sigheaders - but not the plainheaders. -*/ - -gstring * -arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr) -{ -const uschar * identity, * selector, * privkey, * opts, * s; -unsigned options = 0; -int sep = 0; -header_line * headers; -hdr_rlist * rheaders; -blob ar; -int instance; -gstring * g = NULL; -pdkim_bodyhash * b; - -expire = now = 0; - -/* Parse the signing specification */ - -if (!(identity = string_nextinlist(&signspec, &sep, NULL, 0)) || !*identity) - { s = US"identity"; goto bad_arg_ret; } -if (!(selector = string_nextinlist(&signspec, &sep, NULL, 0)) || !*selector) - { s = US"selector"; goto bad_arg_ret; } -if (!(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey) - { s = US"privkey"; goto bad_arg_ret; } -if (!arc_valid_id(identity)) - { s = US"identity"; goto bad_arg_ret; } -if (!arc_valid_id(selector)) - { s = US"selector"; goto bad_arg_ret; } -if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey))) - goto ret_sigheaders; - -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(CS ++s))) - expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA; - if (!now) now = time(NULL); - expire += now; - } - else - expire = (time_t)atol(CS 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. -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 = &headers_rlist; *rp; ) rp = &(*rp)->prev; - *rp = rheaders; - } - -/* 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; - 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, "ARC: no Authentication-Results header for signing"); - goto ret_sigheaders; - } - -/* 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 -*/ - -g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar); - -/* -- Generate AMS - - Looks fairly like a DKIM sig - - Cover all DKIM sig headers as well as the usuals - - ? oversigning? - - Covers the data - - we must have requested a suitable bodyhash previously -XXX so where was that done? I don't see it! -XXX ah, ok - the smtp tpt calls arc_ams_setup_sign_bodyhash() directly, early - -> should pref use a better named call to make the point, but that - can wait until arc becomes a module -*/ - -b = arc_ams_setup_sign_bodyhash(); -g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector, - &b->bh, headers_rlist, privkey, options); - -/* -- Generate AS - - no body coverage - - no h= tag; implicit coverage - - arc status from A-R - - if fail: - - coverage is just the new ARC set - including self (but with an empty b= in self) - - if non-fail: - - all ARC set headers, set-number order, aar then ams then as, - including self (but with an empty b= in self) -*/ - -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. */ - -if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr); - -out: - if (!g) return string_get(1); - (void) string_from_gstring(g); - gstring_release_unused(g); - return g; - - -bad_arg_ret: - log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)", s); -ret_sigheaders: - g = sigheaders; - goto out; -} - - -/******************************************************************************/ - -/* Check to see if the line is an AMS and if so, set up to validate it. -Called from the DKIM input processing. This must be done now as the message -body data is hashed during input. - -We call the DKIM code to request a body-hash; it has the facility already -and the hash parameters might be common with other requests. -*/ - -static const uschar * -arc_header_vfy_feed(gstring * g) -{ -header_line h; -arc_line al; -pdkim_bodyhash * b; -uschar * errstr; - -if (strncmpic(ARC_HDR_AMS, g->s, ARC_HDRLEN_AMS) != 0) return US"not AMS"; - -DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n"); -/* Parse the AMS header */ - -memset(&al, 0, sizeof(arc_line)); -h.next = NULL; -h.slen = len_string_from_gstring(g, &h.text); -if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, le_all))) - { - DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr); - goto badline; - } - -if (!al.a_hash.data) - { - DEBUG(D_acl) debug_printf("ARC: no a_hash from '%.*s'\n", h.slen, h.text); - goto badline; - } - -/* defaults */ -if (!al.c.data) - { - al.c_body.data = US"simple"; al.c_body.len = 6; - al.c_head = al.c_body; - } - -/* Ask the dkim code to calc a bodyhash with those specs */ - -if (!(b = arc_ams_setup_vfy_bodyhash(&al))) - return US"dkim hash setup fail"; - -/* Discard the reference; search again at verify time, knowing that one -should have been created here. */ - -return NULL; - -badline: - return US"line parsing error"; -} - - - -/* A header line has been identified by DKIM processing. - -Arguments: - g Header line - is_vfy TRUE for verify mode or FALSE for signing mode - -Return: - NULL for success, or an error string (probably unused) -*/ - -const uschar * -arc_header_feed(gstring * g, BOOL is_vfy) -{ -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, 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); - } -if (!g) return US""; -return string_from_gstring(g); -} - - -/* Construct an Authentication-Results header portion, for the ARC module */ - -gstring * -authres_arc(gstring * g) -{ -if (arc_state) - { - arc_line * highest_ams; - int start = 0; /* Compiler quietening */ - DEBUG(D_acl) start = gstring_length(g); - - g = string_append(g, 2, US";\n\tarc=", arc_state); - if (arc_received_instance > 0) - { - g = string_fmt_append(g, " (i=%d)", arc_received_instance); - 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); - - g = string_fmt_append(g, " arc.oldest-pass=%d", arc_oldest_pass); - - if (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:\tauthres '%.*s'\n", - gstring_length(g) - start - 3, g->s + start + 3); - } -else - DEBUG(D_acl) debug_printf("ARC:\tno authres\n"); -return g; -} - - -# ifdef SUPPORT_DMARC -/* Append a DMARC history record pair for ARC, to the given history set */ - -gstring * -arc_dmarc_hist_append(gstring * g) -{ -if (arc_state) - { - BOOL first = TRUE; - int i = Ustrcmp(arc_state, "pass") == 0 ? ARES_RESULT_PASS - : Ustrcmp(arc_state, "fail") == 0 ? ARES_RESULT_FAIL - : ARES_RESULT_UNKNOWN; - g = string_fmt_append(g, "arc %d\n", i); - g = string_fmt_append(g, "arc_policy %d json[", - i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS - : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL - : DMARC_ARC_POLICY_RESULT_UNUSED); - /*XXX would we prefer this backwards? */ - for (arc_set * as = arc_verify_ctx.arcset_chain; as; - as = as->next, first = FALSE) - { - arc_line * line = as->hdr_as; - if (line) - { - blob * d = &line->d; - blob * s = &line->s; - - if (!first) - g = string_catn(g, US",", 1); - - g = string_fmt_append(g, " (\"i\":%u," /*)*/ - " \"d\":\"%.*s\"," - " \"s\":\"%.*s\"", - as->instance, - d->data ? (int)d->len : 0, d->data && d->len ? d->data : US"", - s->data ? (int)s->len : 0, s->data && s->len ? s->data : US"" - ); - if ((line = as->hdr_aar)) - { - blob * ip = &line->ip; - if (ip->data && ip->len) - g = string_fmt_append(g, ", \"ip\":\"%.*s\"", (int)ip->len, ip->data); - } - - g = string_catn(g, US")", 1); - } - } - g = string_catn(g, US" ]\n", 3); - } -else - g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n", - ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED); -return g; -} -# endif - - -# endif /* DISABLE_DKIM */ -#endif /* EXPERIMENTAL_ARC */ -/* vi: aw ai sw=2 - */ diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index d602886a0..20a288d66 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -169,6 +169,7 @@ Do not put spaces between # and the 'define'. /* Required to support dynamic-module build */ #define SUPPORT_DKIM +#define SUPPORT_ARC #define SYSLOG_LOG_PID #define SYSLOG_LONG_LINES diff --git a/src/src/drtables.c b/src/src/drtables.c index 61ced3e6a..32765aedc 100644 --- a/src/src/drtables.c +++ b/src/src/drtables.c @@ -441,6 +441,7 @@ if (mi->init && mi->init(mi)) } else DEBUG(D_any) debug_printf_indent("module init call failed for %s\n", mi->name); +/* fprintf(stderr,"misc_mod_add: added %s\n", mi->name); */ } @@ -746,6 +747,9 @@ extern misc_module_info dmarc_module_info; #if defined(SUPPORT_SPF) && SUPPORT_SPF!=2 extern misc_module_info spf_module_info; #endif +#if defined(EXPERIMENTAL_ARC) && (!defined(SUPPORT_ARC) || SUPPORT_ARC!=2) +extern misc_module_info arc_module_info; +#endif void init_misc_mod_list(void) @@ -755,14 +759,17 @@ if (onetime) return; onetime = TRUE; #if !defined(DISABLE_DKIM) && (!defined(SUPPORT_DKIM) || SUPPORT_DKIM!=2) -misc_mod_add(&dkim_module_info); + misc_mod_add(&dkim_module_info); #endif #if defined(SUPPORT_SPF) && SUPPORT_SPF!=2 -misc_mod_add(&spf_module_info); + misc_mod_add(&spf_module_info); #endif #if defined(SUPPORT_DMARC) && SUPPORT_DMARC!=2 /* dmarc depends on spf so this add must go after, for the both-static case */ -misc_mod_add(&dmarc_module_info); + misc_mod_add(&dmarc_module_info); +#endif +#if defined(EXPERIMENTAL_ARC) && (!defined(SUPPORT_ARC) || SUPPORT_ARC!=2) + misc_mod_add(&arc_module_info); #endif } diff --git a/src/src/exim.c b/src/src/exim.c index ca98e25de..2349260df 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -4211,9 +4211,6 @@ is equivalent to the ability to modify a setuid binary! This needs to happen before we read the main configuration. */ init_lookup_list(); init_misc_mod_list(); -#ifdef EXPERIMENTAL_ARC -arc_init(); /*XXX temporary, until we do an arc module */ -#endif /*XXX this excrescence could move to the testsuite standard config setup file */ #ifdef SUPPORT_I18N diff --git a/src/src/exim.h b/src/src/exim.h index 8260dc75f..f5043aea9 100644 --- a/src/src/exim.h +++ b/src/src/exim.h @@ -555,6 +555,9 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly. # include "miscmods/dmarc_api.h" # include #endif +#ifdef EXPERIMENTAL_ARC +# include "miscmods/arc_api.h" +#endif /* The following stuff must follow the inclusion of config.h because it requires various things that are set therein. */ diff --git a/src/src/expand.c b/src/src/expand.c index 02680771f..a41ba98cf 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -454,10 +454,10 @@ static var_entry var_table[] = { { "address_file", vtype_stringptr, &address_file }, { "address_pipe", vtype_stringptr, &address_pipe }, #ifdef EXPERIMENTAL_ARC - { "arc_domains", vtype_string_func, (void *) &fn_arc_domains }, - { "arc_oldest_pass", vtype_int, &arc_oldest_pass }, - { "arc_state", vtype_stringptr, &arc_state }, - { "arc_state_reason", vtype_stringptr, &arc_state_reason }, + { "arc_domains", vtype_module, US"arc" }, + { "arc_oldest_pass", vtype_module, US"arc" }, + { "arc_state", vtype_module, US"arc" }, + { "arc_state_reason", vtype_module, US"arc" }, #endif { "authenticated_fail_id",vtype_stringptr, &authenticated_fail_id }, { "authenticated_id", vtype_stringptr, &authenticated_id }, @@ -4890,9 +4890,6 @@ while (*s) yield = authres_iprev(yield); yield = authres_smtpauth(yield); yield = misc_mod_authres(yield); -#ifdef EXPERIMENTAL_ARC - yield = authres_arc(yield); -#endif break; } diff --git a/src/src/functions.h b/src/src/functions.h index 4f4e615ca..deec54590 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -1,4 +1,3 @@ -extern BOOL arc_init(void); /************************************************* * Exim - an Internet mail transport agent * *************************************************/ @@ -111,18 +110,6 @@ extern void acl_var_write(uschar *, uschar *, void *); extern void add_driver_info(driver_info **, const driver_info *, size_t); -#ifdef EXPERIMENTAL_ARC -# ifdef SUPPORT_DMARC -extern gstring *arc_dmarc_hist_append(gstring *); -# endif -extern void *arc_ams_setup_sign_bodyhash(void); -extern const uschar *arc_header_feed(gstring *, BOOL); -extern gstring *arc_sign(const uschar *, gstring *, uschar **); -extern void arc_sign_init(void); -extern const uschar *acl_verify_arc(void); -extern uschar * fn_arc_domains(void); -#endif - extern void assert_no_variables(void *, int, const char *, int); extern int auth_call_pam(const uschar *, uschar **); extern int auth_call_pwcheck(uschar *, uschar **); @@ -142,9 +129,6 @@ extern int auth_read_input(const uschar *); extern gstring * auth_show_supported(gstring *); extern uschar *authenticator_current_name(void); -#ifdef EXPERIMENTAL_ARC -extern gstring *authres_arc(gstring *); -#endif extern gstring *authres_smtpauth(gstring *); extern uschar *b64encode(const uschar *, int); diff --git a/src/src/globals.c b/src/src/globals.c index 6fae1582f..c65ddf413 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -615,14 +615,6 @@ tree_node *addresslist_anchor = NULL; int addresslist_count = 0; gid_t *admin_groups = NULL; -#ifdef EXPERIMENTAL_ARC -struct arc_set *arc_received = NULL; -int arc_received_instance = 0; -int arc_oldest_pass = 0; -const uschar *arc_state = NULL; -const uschar *arc_state_reason = NULL; -#endif - uschar *authenticated_fail_id = NULL; uschar *authenticated_id = NULL; uschar *authenticated_sender = NULL; diff --git a/src/src/globals.h b/src/src/globals.h index 1f03cefee..2f2f023e3 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -360,13 +360,6 @@ extern int addresslist_count; /* Number defined */ extern gid_t *admin_groups; /* List of admin groups */ extern BOOL allow_domain_literals; /* As it says */ extern BOOL allow_mx_to_ip; /* Allow MX records to -> ip address */ -#ifdef EXPERIMENTAL_ARC -extern struct arc_set *arc_received; /* highest ARC instance evaluation struct */ -extern int arc_received_instance; /* highest ARC instance number in headers */ -extern int arc_oldest_pass; /* lowest passing instance number in headers */ -extern const uschar *arc_state; /* verification state */ -extern const uschar *arc_state_reason; -#endif extern BOOL allow_utf8_domains; /* For experimenting */ extern uschar *authenticated_fail_id; /* ID that failed authentication */ extern uschar *authenticated_id; /* ID that was authenticated */ diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile index 3013a88da..64a66276f 100644 --- a/src/src/miscmods/Makefile +++ b/src/src/miscmods/Makefile @@ -31,6 +31,7 @@ miscmods.a: $(OBJ) # Note that the sources from pdkim/ are linked into the build.../miscmods/ dir # by scripts/Makelinks. +arc.o arc.so: $(HDRS) pdkim.h arc.c dkim.o dkim.so: $(HDRS) dkim.h dkim.c dkim_transport.c \ crypt_ver.h pdkim.h pdkim_hash.h pdkim.c \ signing.h signing.c diff --git a/src/src/miscmods/arc.c b/src/src/miscmods/arc.c new file mode 100644 index 000000000..db546e1ab --- /dev/null +++ b/src/src/miscmods/arc.c @@ -0,0 +1,2171 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ +/* Experimental ARC support for Exim + Copyright (c) The Exim Maintainers 2021 - 2024 + Copyright (c) Jeremy Harris 2018 - 2020 + License: GPL + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "../exim.h" +#if defined EXPERIMENTAL_ARC +# if defined DISABLE_DKIM +# error DKIM must also be enabled for ARC +# else + +# include "../functions.h" +# include "pdkim.h" +# include "signing.h" + +/* Globals */ + +struct arc_set *arc_received = NULL; /* highest ARC instance eval struct */ +int arc_received_instance = 0; /* highest ARC instance num in hdrs */ +int arc_oldest_pass = 0; /* lowest passing inst num in hdrs */ +const uschar *arc_state = NULL; /* verification state */ +const uschar *arc_state_reason = NULL; + +/******************************************************************************/ +#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 { + struct hdr_rlist * prev; + BOOL used; + header_line * h; +} hdr_rlist; + +typedef struct arc_line { + header_line * complete; /* including the header name; nul-term */ + uschar * relaxed; + + /* identified tag contents */ + /*XXX t= for AS? */ + blob i; + blob cv; + blob a; + blob b; + blob bh; + blob d; + blob h; + blob s; + blob c; + blob l; + blob ip; + + /* tag content sub-portions */ + blob a_algo; + blob a_hash; + + blob c_head; + blob c_body; + + /* modified copy of b= field in line */ + blob rawsig_no_b_val; +} arc_line; + +typedef struct arc_set { + struct arc_set * next; + struct arc_set * prev; + + unsigned instance; + arc_line * hdr_aar; + arc_line * hdr_ams; + arc_line * hdr_as; + + const uschar * ams_verify_done; + BOOL ams_verify_passed; +} arc_set; + +typedef struct arc_ctx { + arc_set * arcset_chain; + arc_set * arcset_chain_last; +} arc_ctx; + +#define ARC_HDR_AAR US"ARC-Authentication-Results:" +#define ARC_HDRLEN_AAR 27 +#define ARC_HDR_AMS US"ARC-Message-Signature:" +#define ARC_HDRLEN_AMS 22 +#define ARC_HDR_AS US"ARC-Seal:" +#define ARC_HDRLEN_AS 9 +#define HDR_AR US"Authentication-Results:" +#define HDRLEN_AR 23 + +typedef enum line_extract { + le_instance_only, + le_instance_plus_ip, + le_all +} line_extract_t; + +static misc_module_info * arc_dkim_mod_info; + +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 }; + +/* We build a context for either Sign or Verify. + +For Verify, it's a fresh new one for ACL verify=arc - there is no connection +with the single line handling done during reception via the DKIM feed. + +For Verify we do it twice; initially during reception (via the DKIM feed) +and then later for the full verification. + +The former only looks at AMS headers, to discover what hash(es) we need done for +ARC on the message body; we call back to the DKIM code to set up so that it does +them for us during reception. That call needs info from many of the AMS tags; +arc_parse_line() for only the AMS is called asking for all the tag types. +That context is then discarded. + +Later, for Verify, we look at ARC headers again and then grab the hash result +from the DKIM layer. arc_parse_line() is called for all 3 line types, +gathering info for only 'i' and 'ip' tags from AAR headers, +for all tag types from AMS and AS headers. + + +For Sign, while running through the existing headers (before adding any for +this signing operation, we "take copies" of the headers, we call +arc_parse_line() gathering only the 'i' tag (instance) information. +*/ + + +/******************************************************************************/ + +/* We need a module init function, to check on the dkim module being present +(and we may as well stash it's modinfo ptr) +*/ + +static BOOL +arc_init(void * dummy) +{ +uschar * errstr = NULL; +if ((arc_dkim_mod_info = misc_mod_find(US"dkim", &errstr))) + return TRUE; +log_write(0, LOG_MAIN, "arc: %s", errstr); +return FALSE; +} + +static void +arc_smtp_reset(void) +{ +arc_state = arc_state_reason = NULL; +arc_received_instance = 0; +} + +/******************************************************************************/ + + +/* Get the instance number from the header. +Return 0 on error */ +static unsigned +arc_instance_from_hdr(const arc_line * al) +{ +const uschar * s = al->i.data; +if (!s || !al->i.len) return 0; +return (unsigned) atoi(CCS s); +} + + +static uschar * +skip_fws(uschar * s) +{ +uschar c = *s; +while (c && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) c = *++s; +return s; +} + + +/* Locate instance struct on chain, inserting a new one if +needed. The chain is in increasing-instance-number order +by the "next" link, and we have a "prev" link also. +*/ + +static arc_set * +arc_find_set(arc_ctx * ctx, unsigned i) +{ +arc_set ** pas, * as, * next, * prev; + +for (pas = &ctx->arcset_chain, prev = NULL, next = ctx->arcset_chain; + as = *pas; pas = &as->next) + { + if (as->instance > i) break; + if (as->instance == i) + { + DEBUG(D_acl) debug_printf("ARC: existing instance %u\n", i); + return as; + } + next = as->next; + prev = as; + } + +DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i); +*pas = as = store_get(sizeof(arc_set), GET_UNTAINTED); +memset(as, 0, sizeof(arc_set)); +as->next = next; +as->prev = prev; +as->instance = i; +if (next) + next->prev = as; +else + ctx->arcset_chain_last = as; +return as; +} + + + +/* Insert a tag content into the line structure. +Note this is a reference to existing data, not a copy. +Check for already-seen tag. +The string-pointer is on the '=' for entry. Update it past the +content (to the ;) on return; +*/ + +static uschar * +arc_insert_tagvalue(arc_line * al, unsigned loff, uschar ** ss) +{ +uschar * s = *ss; +uschar c = *++s; +blob * b = (blob *)(US al + loff); +size_t len = 0; + +/* [FWS] tag-value [FWS] */ + +if (b->data) return US"fail"; +s = skip_fws(s); /* FWS */ + +b->data = s; +while ((c = *s) && c != ';') { len++; s++; } +*ss = s; +while (len && ((c = s[-1]) == ' ' || c == '\t' || c == '\n' || c == '\r')) + { s--; len--; } /* FWS */ +b->len = len; +return NULL; +} + + +/* Inspect a header line, noting known tag fields. +Check for duplicate named tags. + +See the file block comment for how this is used. + +Return: NULL for good, or an error string +*/ + +static uschar * +arc_parse_line(arc_line * al, header_line * h, unsigned off, line_extract_t l_ext) +{ +uschar * s = h->text + off; +uschar * r = NULL; +uschar c; + +al->complete = h; + +if (l_ext == le_all) /* need to grab rawsig_no_b */ + { + al->rawsig_no_b_val.data = store_get(h->slen + 1, GET_TAINTED); + memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */ + r = al->rawsig_no_b_val.data + off; + al->rawsig_no_b_val.len = off; + } + +/* tag-list = tag-spec *( ";" tag-spec ) [ ";" ] */ + +while ((c = *s)) + { + char tagchar; + uschar * t; + unsigned i = 0; + uschar * fieldstart = s; + uschar * bstart = NULL, * bend; + + /* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */ + /*X or just a naked FQDN, in a AAR ! */ + + s = skip_fws(s); /* leading FWS */ + if (!*s) break; + tagchar = *s++; + if (!*(s = skip_fws(s))) break; /* FWS */ + + switch (tagchar) + { + case 'a': /* a= AMS algorithm */ + if (l_ext == le_all && *s == '=') + { + if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup"; + + /* substructure: algo-hash (eg. rsa-sha256) */ + + t = al->a_algo.data = al->a.data; + while (*t != '-') + if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value"; + al->a_algo.len = i; + if (*t++ != '-') return US"no '-' in 'a' value"; + al->a_hash.data = t; + al->a_hash.len = al->a.len - i - 1; + } + break; + case 'b': + if (l_ext == le_all) + { + gstring * g = NULL; + + switch (*s) + { + case '=': /* b= AMS signature */ + if (al->b.data) return US"already b data"; + bstart = s+1; + + /* The signature can have FWS inserted in the content; + make a stripped copy */ + + while ((c = *++s) && c != ';') + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + g = string_catn(g, s, 1); + if (!g) return US"no b= value"; + al->b.len = len_string_from_gstring(g, &al->b.data); + gstring_release_unused(g); + bend = s; + break; + case 'h': /* bh= AMS body hash */ + s = skip_fws(++s); /* FWS */ + if (*s == '=') + { + if (al->bh.data) return US"already bh data"; + + /* The bodyhash can have FWS inserted in the content; + make a stripped copy */ + + while ((c = *++s) && c != ';') + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + g = string_catn(g, s, 1); + if (!g) return US"no bh= value"; + al->bh.len = len_string_from_gstring(g, &al->bh.data); + gstring_release_unused(g); + } + break; + default: + return US"b? tag"; + } + } + break; + case 'c': + if (l_ext == le_all) switch (*s) + { + case '=': /* c= AMS canonicalisation */ + if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup"; + + /* substructure: head/body (eg. relaxed/simple)) */ + + t = al->c_head.data = al->c.data; + while (isalpha(*t)) + if (!*t++ || ++i > al->a.len) break; + al->c_head.len = i; + if (*t++ == '/') /* /body is optional */ + { + al->c_body.data = t; + al->c_body.len = al->c.len - i - 1; + } + else + { + al->c_body.data = US"simple"; + al->c_body.len = 6; + } + break; + case 'v': /* cv= AS validity */ + s = skip_fws(s); + if (*++s == '=') + if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) + return US"cv tag dup"; + break; + } + break; + case 'd': /* d= AMS domain */ + if (l_ext == le_all && *s == '=') + if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) + return US"d tag dup"; + break; + case 'h': /* h= AMS headers */ + if (*s == '=') + if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) + return US"h tag dup"; + break; + case 'i': /* i= ARC set instance */ + if (*s == '=') + { + if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) + return US"i tag dup"; + if (l_ext == le_instance_only) + goto done; /* early-out */ + } + break; + case 'l': /* l= bodylength */ + if (l_ext == le_all && *s == '=') + if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) + return US"l tag dup"; + break; + case 's': + if (*s == '=' && l_ext == le_all) + { + if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) + return US"s tag dup"; + } + else if ( l_ext == le_instance_plus_ip + && Ustrncmp(s, "mtp.remote-ip", 13) == 0) + { /* smtp.remote-ip= AAR reception data */ + s += 13; + s = skip_fws(s); + if (*s != '=') return US"smtp.remote_ip tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, ip), &s)) + return US"ip tag dup"; + } + break; + } + + while ((c = *s) && c != ';') s++; /* end of this tag=value */ + if (c) s++; /* ; after tag-spec */ + + /* for all but the b= tag, copy the field including FWS. For the b=, + drop the tag content. */ + + if (r) + if (bstart) + { + size_t n = bstart - fieldstart; + memcpy(r, fieldstart, n); /* FWS "b=" */ + r += n; + al->rawsig_no_b_val.len += n; + n = s - bend; + memcpy(r, bend, n); /* FWS ";" */ + r += n; + al->rawsig_no_b_val.len += n; + } + else + { + size_t n = s - fieldstart; + memcpy(r, fieldstart, n); + r += n; + al->rawsig_no_b_val.len += n; + } + } + +if (r) + *r = '\0'; + +done: +/* debug_printf("%s: finshed\n", __FUNCTION__); */ +return NULL; +} + + +/* Insert one header line in the correct set of the chain, +adding instances as needed and checking for duplicate lines. +*/ + +static uschar * +arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff, + line_extract_t l_ext, arc_line ** alp_ret) +{ +unsigned i; +arc_set * as; +arc_line * al = store_get(sizeof(arc_line), GET_UNTAINTED), ** alp; +uschar * e; + +memset(al, 0, sizeof(arc_line)); + +if ((e = arc_parse_line(al, h, off, l_ext))) + { + DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e); + return string_sprintf("line parse: %s", e); + } +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"; + +*alp = al; +if (alp_ret) *alp_ret = al; +return NULL; +} + + + +/* Called for both Sign and Verify */ + +static const uschar * +arc_try_header(arc_ctx * ctx, header_line * h, BOOL is_signing) +{ +const uschar * e; + +/*debug_printf("consider hdr '%s'\n", h->text);*/ +if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0) + { + DEBUG(D_acl) + { + int len = h->slen; + uschar * s; + for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) + s--, len--; + debug_printf("ARC: found AAR: %.*s\n", len, h->text); + } + if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar), + is_signing ? le_instance_only : le_instance_plus_ip, NULL))) + { + DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e); + return string_sprintf("inserting AAR: %s", e); + } + } +else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0) + { + arc_line * ams; + + DEBUG(D_acl) + { + int len = h->slen; + uschar * s; + for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) + s--, len--; + debug_printf("ARC: found AMS: %.*s\n", len, h->text); + } + if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams), + is_signing ? le_instance_only : le_all, &ams))) + { + DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e); + return string_sprintf("inserting AMS: %s", e); + } + + /* defaults */ + if (!ams->c.data) + { + ams->c_head.data = US"simple"; ams->c_head.len = 6; + ams->c_body = ams->c_head; + } + } +else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0) + { + DEBUG(D_acl) + { + int len = h->slen; + uschar * s; + for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) + s--, len--; + debug_printf("ARC: found AS: %.*s\n", len, h->text); + } + if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as), + is_signing ? le_instance_only : le_all, NULL))) + { + DEBUG(D_acl) debug_printf("inserting AS: %s\n", e); + return string_sprintf("inserting AS: %s", e); + } + } +return NULL; +} + + + +/* Gather the chain of arc sets from the headers. +Check for duplicates while that is done. Also build the +reverse-order headers list. +Called on an ACL verify=arc condition. + +Return: ARC state if determined, eg. by lack of any ARC chain. +*/ + +static const uschar * +arc_vfy_collect_hdrs(arc_ctx * ctx) +{ +header_line * h; +hdr_rlist * r = NULL, * rprev = NULL; +const uschar * e; + +DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n"); +for (h = header_list; h; h = h->next) + { + r = store_get(sizeof(hdr_rlist), GET_UNTAINTED); + r->prev = rprev; + r->used = FALSE; + r->h = h; + rprev = r; + + if ((e = arc_try_header(ctx, h, FALSE))) + { + arc_state_reason = string_sprintf("collecting headers: %s", e); + return US"fail"; + } + } +headers_rlist = r; + +if (!ctx->arcset_chain) return US"none"; +return NULL; +} + + +static BOOL +arc_cv_match(arc_line * al, const uschar * s) +{ +return Ustrncmp(s, al->cv.data, al->cv.len) == 0; +} + +/******************************************************************************/ +/* Service routines provided by the dkim module */ + +static int +arc_dkim_hashname_blob_to_type(const blob * name) +{ +typedef int (*fn_t)(const blob *); +return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HASHNAME_TO_TYPE]) (name); +} +static hashmethod +arc_dkim_hashtype_to_method(int hashtype) +{ +typedef hashmethod (*fn_t)(int); +return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HASHTYPE_TO_METHOD]) (hashtype); +} +static hashmethod +arc_dkim_hashname_blob_to_method(const blob * name) +{ +typedef hashmethod (*fn_t)(const blob *); +return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HASHNAME_TO_METHOD]) (name); +} + +/******************************************************************************/ + +/* Do a "relaxed" canonicalization of a header */ +static uschar * +arc_relax_header_n(const uschar * text, int len, BOOL append_crlf) +{ +typedef uschar * (*fn_t)(const uschar *, int, BOOL); +return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_HEADER_RELAX]) + (text, len, append_crlf); +} + + + +/* Return the hash of headers from the message that the AMS claims it +signed. +*/ + +static void +arc_get_verify_hhash(arc_ctx * ctx, arc_line * ams, blob * hhash) +{ +const uschar * headernames = string_copyn(ams->h.data, ams->h.len); +const uschar * hn; +int sep = ':'; +hdr_rlist * r; +BOOL relaxed = Ustrncmp(US"relaxed", ams->c_head.data, ams->c_head.len) == 0; +hashmethod hm = arc_dkim_hashname_blob_to_method(&ams->a_hash); +hctx hhash_ctx; +const uschar * s; +int len; + +if (hm < 0 || !exim_sha_init(&hhash_ctx, hm)) + { + DEBUG(D_acl) + debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); + return; + } + +/* For each headername in the list from the AMS (walking in order) +walk the message headers in reverse order, adding to the hash any +found for the first time. For that last point, maintain used-marks +on the list of message headers. */ + +DEBUG(D_acl) debug_printf("ARC: AMS header data for verification:\n"); + +for (r = headers_rlist; r; r = r->prev) + r->used = FALSE; +while ((hn = string_nextinlist(&headernames, &sep, NULL, 0))) + for (r = headers_rlist; r; r = r->prev) + if ( !r->used + && strncasecmp(CCS (s = r->h->text), CCS hn, Ustrlen(hn)) == 0 + ) + { + if (relaxed) s = arc_relax_header_n(s, r->h->slen, TRUE); + + DEBUG(D_acl) debug_printf("%Z\n", s); + exim_sha_update_string(&hhash_ctx, s); + r->used = TRUE; + break; + } + +/* 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 = arc_relax_header_n(s, len, FALSE)); +DEBUG(D_acl) debug_printf("%.*Z\n", len, s); +exim_sha_update(&hhash_ctx, s, len); + +exim_sha_finish(&hhash_ctx, hhash); +DEBUG(D_acl) + { debug_printf("ARC: header hash: %.*H\n", hhash->len, hhash->data); } +return; +} + + + + +static blob * +arc_line_to_pubkey(arc_line * al, const uschar ** errstr) +{ +typedef const uschar * (*fn_t)(const uschar *, blob **, const uschar **); +blob * pubkey; +const uschar * hashes; +const uschar * srvtype = + (((fn_t *) arc_dkim_mod_info->functions)[DKIM_DNS_PUBKEY]) + (string_sprintf("%b._domainkey.%b", &al->s, &al->d), &pubkey, &hashes); + +if (!srvtype) + { *errstr = US"pubkey dns lookup fail"; return NULL; } +if ((Ustrcmp(srvtype, "*") != 0 && Ustrcmp(srvtype, "email") != 0)) + { + *errstr = string_sprintf("pubkey format error: srvtype '%s'", srvtype); + return NULL; + } + +/* If the pubkey limits use to specified hashes, reject unusable +signatures. XXX should we have looked for multiple dns records? */ + +if (hashes) + { + const uschar * list = hashes, * ele; + int sep = ':'; + + while ((ele = string_nextinlist(&list, &sep, NULL, 0))) + if (Ustrncmp(ele, al->a_hash.data, al->a_hash.len) == 0) break; + if (!ele) + { + DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%b\n", hashes, &al->a); + *errstr = US"no usable sig for this pubkey hash list"; + return NULL; + } + } +return pubkey; +} + + + + +/* Set up a body hashing method on the given signature-context +(creates a new one if needed, or uses an already-present one). + +Arguments: + signing TRUE for signing, FALSE for verification + c canonicalization spec, text form + ah hash, text form + bodylen byte count for message body + +Return: pointer to hashing method struct +*/ + +static pdkim_bodyhash * +arc_set_bodyhash(BOOL signing, + const blob * c, const blob * ah, long bodylen) +{ +typedef pdkim_bodyhash * (*fn_t)(BOOL, + const blob * canon, const blob * hash, long bodylen); + +return (((fn_t *) arc_dkim_mod_info->functions)[DKIM_SET_BODYHASH]) + (signing, c, ah, bodylen); +} + + + + +static pdkim_bodyhash * +arc_ams_setup_vfy_bodyhash(arc_line * ams) +{ +blob * c = &ams->c; +long bodylen = ams->l.data + ? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) + : -1; + +if (!c->data) + { + c->data = US"simple"; /* RFC 6376 (DKIM) default */ + c->len = 6; + } + +return arc_set_bodyhash(FALSE, c, &ams->a_hash, bodylen); +} + + + +static void +arc_decode_base64(const uschar * str, blob * b) +{ +int dlen = b64decode(str, &b->data, str); +if (dlen < 0) b->data = NULL; +b->len = dlen; +} + + + +static int +arc_sig_verify(arc_set * as, arc_line * al, hashmethod hm, + blob * hhash_computed, blob * sighash, + const uschar * why, const uschar ** errstr_p) +{ +blob * pubkey; +const uschar * errstr = NULL; +int rc; +typedef int (*fn_t) + (const blob *, const blob *, hashmethod, const blob *, const uschar **); + +/* Get the public key from DNS */ + +/*XXX dkim module */ +if (!(pubkey = arc_line_to_pubkey(al, &errstr))) + { + *errstr_p = string_sprintf("%s (%s)", errstr, why); + return ERROR; + } + +rc = (((fn_t *) arc_dkim_mod_info->functions)[DKIM_SIG_VERIFY]) + (sighash, hhash_computed, hm, pubkey, &errstr); +switch (rc) + { + case OK: + break; + case FAIL: + DEBUG(D_acl) + debug_printf("ARC i=%d %s verify %s\n", as->instance, why, errstr); + break; + case ERROR: + DEBUG(D_acl) debug_printf("ARC verify %s init: %s\n", why, errstr); + break; + } +return rc; +} + + + + +/* Verify an AMS. This is a DKIM-sig header, but with an ARC i= tag +and without a DKIM v= tag. +*/ + +static const uschar * +arc_ams_verify(arc_ctx * ctx, arc_set * as) +{ +arc_line * ams = as->hdr_ams; +pdkim_bodyhash * b; +blob sighash; +blob hhash_computed; +hashmethod hm; +const uschar * errstr; +int rc; + +as->ams_verify_done = US"in-progress"; + +/* Check the AMS has all the required tags: + "a=" algorithm + "b=" signature + "bh=" body hash + "d=" domain (for key lookup) + "h=" headers (included in signature) + "s=" key-selector (for key lookup) +*/ +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) + { + debug_printf("ARC i=%d AMS Body bytes hashed: %lu\n" + " Body %b computed: %.*H\n", + as->instance, b->signed_body_bytes, + &ams->a_hash, b->bh.len, b->bh.data); + } + +/* We know the bh-tag blob is of a nul-term string, so safe as a string */ + +if ( !ams->bh.data + || (arc_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len) + || memcmp(sighash.data, b->bh.data, b->bh.len) != 0 + ) + { + DEBUG(D_acl) + { + debug_printf("ARC i=%d AMS Body hash from headers: ", as->instance); + debug_printf("%.*H\n", sighash.len, sighash.data); + debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance); + } + 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); + +/* We know the b-tag blob is of a nul-term string, so safe as a string */ +arc_decode_base64(ams->b.data, &sighash); + +arc_get_verify_hhash(ctx, ams, &hhash_computed); + +if ((hm = arc_dkim_hashname_blob_to_method(&ams->a_hash)) < 0) + { + DEBUG(D_acl) debug_printf("ARC i=%d AMS verify bad a_hash\n", as->instance); + return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify"; + } + +rc = arc_sig_verify(as, ams, hm, &hhash_computed, &sighash, US"AMS", &errstr); +if (rc != OK) + return as->ams_verify_done = arc_state_reason = + rc == FAIL ? US"AMS sig nonverify" : errstr; + +DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance); +as->ams_verify_passed = TRUE; +return NULL; +} + + + +/* Check the sets are instance-continuous and that all +members are present. Check that no arc_seals are "fail". +Set the highest instance number global. +Verify the latest AMS. +*/ +static uschar * +arc_headers_check(arc_ctx * ctx) +{ +arc_set * as; +int inst; +BOOL ams_fail_found = FALSE; + +if (!(as = ctx->arcset_chain_last)) + return US"none"; + +for(inst = as->instance; as; as = as->prev, inst--) + { + 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. */ + + if (!ams_fail_found) + if (arc_ams_verify(ctx, as)) + 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 = arc_received->instance; + +/* We can skip the latest-AMS validation, if we already did it. */ + +as = ctx->arcset_chain_last; +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; +} + + +/******************************************************************************/ +static const uschar * +arc_seal_verify(arc_ctx * ctx, arc_set * as) +{ +arc_line * hdr_as = as->hdr_as; +arc_set * as2; +hashmethod hm; +hctx hhash_ctx; +blob hhash_computed; +blob sighash; +const uschar * errstr; +int rc; + +DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance); +/* + 1. If the value of the "cv" tag on that seal is "fail", the + chain state is "fail" and the algorithm stops here. (This + step SHOULD be skipped if the earlier step (2.1) was + performed) [it was] + + 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv + == "none" && i != 1)) then the chain state is "fail" and the + algorithm stops here (note that the ordering of the logic is + structured for short-circuit evaluation). +*/ + +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 + the ARC-Seal. +*/ + +hm = arc_dkim_hashname_blob_to_method(&hdr_as->a_hash); + +if (hm < 0 || !exim_sha_init(&hhash_ctx, hm)) + { + DEBUG(D_acl) + debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); + arc_state_reason = US"seal hash setup error"; + return US"fail"; + } + +/* + 4. Compute the canonicalized form of the ARC header fields, in + the order described in Section 5.4.2, using the "relaxed" + 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"); +for (as2 = ctx->arcset_chain; + as2 && as2->instance <= as->instance; + as2 = as2->next) + { + arc_line * al; + uschar * s; + int len; + + al = as2->hdr_aar; + if (!(s = al->relaxed)) + al->relaxed = s = arc_relax_header_n(al->complete->text, + al->complete->slen, TRUE); + len = Ustrlen(s); + DEBUG(D_acl) debug_printf("%Z\n", s); + exim_sha_update(&hhash_ctx, s, len); + + al = as2->hdr_ams; + if (!(s = al->relaxed)) + al->relaxed = s = arc_relax_header_n(al->complete->text, + al->complete->slen, TRUE); + len = Ustrlen(s); + DEBUG(D_acl) debug_printf("%Z\n", s); + exim_sha_update(&hhash_ctx, s, len); + + al = as2->hdr_as; + if (as2->instance == as->instance) + s = arc_relax_header_n(al->rawsig_no_b_val.data, + al->rawsig_no_b_val.len, FALSE); + else if (!(s = al->relaxed)) + al->relaxed = s = arc_relax_header_n(al->complete->text, + al->complete->slen, TRUE); + len = Ustrlen(s); + DEBUG(D_acl) debug_printf("%Z\n", s); + exim_sha_update(&hhash_ctx, s, len); + } + +/* + 5. Retrieve the final digest from the hash function. +*/ + +exim_sha_finish(&hhash_ctx, &hhash_computed); +DEBUG(D_acl) + { + debug_printf("ARC i=%d AS Header %b computed: ", + as->instance, &hdr_as->a_hash); + debug_printf("%.*H\n", hhash_computed.len, hhash_computed.data); + } + + +/* + 6. Retrieve the public key identified by the "s" and "d" tags in + the ARC-Seal, as described in Section 4.1.6. + +Done below, in arc_sig_verify(). + + 7. Determine whether the signature portion ("b" tag) of the ARC- + Seal and the digest computed above are valid according to the + public key. (See also Section Section 8.4 for failure case + handling) + + 8. If the signature is not valid, the chain state is "fail" and + the algorithm stops here. +*/ + +/* We know the b-tag blob is of a nul-term string, so safe as a string */ +arc_decode_base64(hdr_as->b.data, &sighash); + +rc = arc_sig_verify(as, hdr_as, hm, &hhash_computed, &sighash, US"AS", &errstr); +if (rc != OK) + { + if (rc == FAIL) arc_state_reason = US"seal sigverify error"; + return US"fail"; + } + +DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d pass\n", as->instance); +return NULL; +} + + +static const uschar * +arc_verify_seals(arc_ctx * ctx) +{ +arc_set * as = ctx->arcset_chain_last; + +if (!as) + return US"none"; + +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; +} +/******************************************************************************/ + +/* Do ARC verification. Called from DATA ACL, on a verify = arc +condition. Set arc_state, and compare with given list of acceptable states. + +Arguments: + condlist list of resulta to test for OK/FAIL return; + NULL for default list + +Return: OK/FAIL, or DEFER on error +*/ + +static int +acl_verify_arc(const uschar * condlist) +{ +const uschar * res; + +memset(&arc_verify_ctx, 0, sizeof(arc_verify_ctx)); + +/* AS evaluation, per +https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6 +*/ +/* 1. Collect all ARC sets currently on the message. If there were + none, the ARC state is "none" and the algorithm stops here. +*/ + +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 + exactly one of each of the three ARC-specific header fields), + then the chain state is "fail" and the algorithm stops here. + + 1. To avoid the overhead of unnecessary computation and delay + from crypto and DNS operations, the cv value for all ARC- + Seal(s) MAY be checked at this point. If any of the values + are "fail", then the overall state of the chain is "fail" and + the algorithm stops here. + + 3. Conduct verification of the ARC-Message-Signature header field + bearing the highest instance number. If this verification fails, + then the chain state is "fail" and the algorithm stops here. +*/ + +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 + following logic: + + 1. If the value of the "cv" tag on that seal is "fail", the + chain state is "fail" and the algorithm stops here. (This + step SHOULD be skipped if the earlier step (2.1) was + performed) + + 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv + == "none" && i != 1)) then the chain state is "fail" and the + algorithm stops here (note that the ordering of the logic is + structured for short-circuit evaluation). + + 3. Initialize a hash function corresponding to the "a" tag of + the ARC-Seal. + + 4. Compute the canonicalized form of the ARC header fields, in + the order described in Section 5.4.2, using the "relaxed" + header canonicalization defined in Section 3.4.2 of + [RFC6376]. Pass the canonicalized result to the hash + function. + + 5. Retrieve the final digest from the hash function. + + 6. Retrieve the public key identified by the "s" and "d" tags in + the ARC-Seal, as described in Section 4.1.6. + + 7. Determine whether the signature portion ("b" tag) of the ARC- + Seal and the digest computed above are valid according to the + public key. (See also Section Section 8.4 for failure case + handling) + + 8. If the signature is not valid, the chain state is "fail" and + the algorithm stops here. + + 5. If all seals pass validation, then the chain state is "pass", and + the algorithm is complete. +*/ + +if ((res = arc_verify_seals(&arc_verify_ctx))) + goto out; + +res = US"pass"; + +out: + { + int csep = 0; + uschar * cond; + + if (!(arc_state = res)) + return DEFER; + + DEBUG(D_acl) debug_printf_indent("ARC verify result %s %s%s%s\n", arc_state, + arc_state_reason ? "(":"", arc_state_reason, arc_state_reason ? ")":""); + + if (!condlist) condlist = US"none:pass"; + while ((cond = string_nextinlist(&condlist, &csep, NULL, 0))) + if (Ustrcmp(res, cond) == 0) return OK; + return FAIL; + } +} + +static BOOL +arc_is_pass(void) +{ +return arc_state && Ustrcmp(arc_state, "pass") == 0; +} + +/******************************************************************************/ + +/* Prepend the header to the rlist */ + +static hdr_rlist * +arc_rlist_entry(hdr_rlist * list, const uschar * s, int len) +{ +hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line), GET_UNTAINTED); +header_line * h = r->h = (header_line *)(r+1); + +r->prev = list; +r->used = FALSE; +h->next = NULL; +h->type = 0; +h->slen = len; +h->text = US s; + +return r; +} + + +/* Walk the given headers strings identifying each header, and construct +a reverse-order list. +*/ + +static hdr_rlist * +arc_sign_scan_headers(arc_ctx * ctx, gstring * sigheaders) +{ +const uschar * s; +hdr_rlist * rheaders = NULL; + +s = sigheaders ? sigheaders->s : NULL; +if (s) while (*s) + { + const uschar * s2 = s; + + /* This works for either NL or CRLF lines; also nul-termination */ + while (*++s2) + if (*s2 == '\n' && s2[1] != '\t' && s2[1] != ' ') break; + s2++; /* move past end of line */ + + rheaders = arc_rlist_entry(rheaders, s, s2 - s); + s = s2; + } +return rheaders; +} + + + +/* Return the A-R content, without identity, with line-ending and +NUL termination. */ + +static BOOL +arc_sign_find_ar(header_line * headers, const uschar * identity, blob * ret) +{ +header_line * h; +int ilen = Ustrlen(identity); + +ret->data = NULL; +for(h = headers; h; h = h->next) + { + uschar * s = h->text, c; + int len = h->slen; + + if (Ustrncmp(s, HDR_AR, HDRLEN_AR) != 0) continue; + s += HDRLEN_AR, len -= HDRLEN_AR; /* header name */ + while ( len > 0 + && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) + s++, len--; /* FWS */ + if (Ustrncmp(s, identity, ilen) != 0) continue; + s += ilen; len -= ilen; /* identity */ + if (len <= 0) continue; + if ((c = *s) && c == ';') s++, len--; /* identity terminator */ + while ( len > 0 + && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) + s++, len--; /* FWS */ + if (len <= 0) continue; + ret->data = s; + ret->len = len; + return TRUE; + } +return FALSE; +} + + + +/* Append a constructed AAR including CRLF. Add it to the arc_ctx too. */ + +static gstring * +arc_sign_append_aar(gstring * g, arc_ctx * ctx, + const uschar * identity, int instance, blob * ar) +{ +int aar_off = gstring_length(g); +arc_set * as = + store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line), GET_UNTAINTED); +arc_line * al = (arc_line *)(as+1); +header_line * h = (header_line *)(al+1); + +g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR); +g = string_fmt_append(g, " i=%d; %s; smtp.remote-ip=%s;\r\n\t%b", + instance, identity, sender_host_address, ar); + +h->slen = g->ptr - aar_off; +h->text = g->s + aar_off; +al->complete = h; +as->next = NULL; +as->prev = ctx->arcset_chain_last; +as->instance = instance; +as->hdr_aar = al; +if (instance == 1) + ctx->arcset_chain = as; +else + ctx->arcset_chain_last->next = as; +ctx->arcset_chain_last = as; + +DEBUG(D_transport) debug_printf("ARC: AAR '%.*s'\n", h->slen - 2, h->text); +return g; +} + + + +static BOOL +arc_sig_from_pseudoheader(gstring * hdata, int hashtype, const uschar * privkey, + blob * sig, const uschar * why) +{ +hashmethod hm = /*sig->keytype == KEYTYPE_ED25519*/ FALSE + ? HASH_SHA2_512 + : arc_dkim_hashtype_to_method(hashtype); + +blob hhash; +const uschar * errstr; +typedef const uschar * (*fn_t) + (const blob *, hashmethod, const uschar *, blob *); + +DEBUG(D_transport) + { + hctx hhash_ctx; + debug_printf("ARC: %s header data for signing:\n", why); + debug_printf("%.*Z\n", hdata->ptr, hdata->s); + + (void) exim_sha_init(&hhash_ctx, hm); + exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); + exim_sha_finish(&hhash_ctx, &hhash); + debug_printf("ARC: header hash: %.*H\n", hhash.len, hhash.data); + } + +if (FALSE /*need hash for Ed25519 or GCrypt signing*/ ) + { + hctx hhash_ctx; + (void) exim_sha_init(&hhash_ctx, arc_dkim_hashtype_to_method(hashtype)); + exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); + exim_sha_finish(&hhash_ctx, &hhash); + } +else + { + hhash.data = hdata->s; + hhash.len = hdata->ptr; + } + +errstr = (((fn_t *) arc_dkim_mod_info->functions)[DKIM_SIGN_DATA]) + (&hhash, hm, privkey, sig); +if (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; +} + + + +static gstring * +arc_sign_append_sig(gstring * g, blob * sig) +{ +/*debug_printf("%s: raw sig %.*H\n", __FUNCTION__, sig->len, sig->data);*/ +sig->data = b64encode(sig->data, sig->len); +sig->len = Ustrlen(sig->data); +for (;;) + { + int len = MIN(sig->len, 74); + g = string_catn(g, sig->data, len); + if ((sig->len -= len) == 0) break; + sig->data += len; + g = string_catn(g, US"\r\n\t ", 5); + } +g = string_catn(g, US";\r\n", 3); +gstring_release_unused(g); +string_from_gstring(g); +return g; +} + + +/* Append a constructed AMS including CRLF. Add it to the arc_ctx too. */ + +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, unsigned options) +{ +uschar * s; +gstring * hdata = NULL; +int col; +const blob ams_h = {.data = US"sha256", .len = 6}; /*XXX hardwired */ +int hashtype = arc_dkim_hashname_blob_to_type(&ams_h); +blob sig; +int ams_off; +arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED); +header_line * h = (header_line *)(al+1); + +/* debug_printf("%s\n", __FUNCTION__); */ + +/* Construct the to-be-signed AMS pseudo-header: everything but the sig. */ + +ams_off = gstring_length(g); +g = string_fmt_append(g, "%s i=%d; a=rsa-sha256; c=relaxed; d=%s; s=%s", + ARC_HDR_AMS, instance, identity, selector); /*XXX hardwired a= */ +if (options & ARC_SIGN_OPT_TSTAMP) + g = string_fmt_append(g, "; t=%lu", (u_long)now); +if (options & ARC_SIGN_OPT_EXPIRE) + g = string_fmt_append(g, "; x=%lu", (u_long)expire); +g = string_fmt_append(g, ";\r\n\tbh=%s;\r\n\th=", + b64encode(bodyhash->data, bodyhash->len)); + +for(col = 3; rheaders; rheaders = rheaders->prev) + { + const uschar * hnames = US"DKIM-Signature:" PDKIM_DEFAULT_SIGN_HEADERS; + uschar * name, * htext = rheaders->h->text; + int sep = ':'; + + /* Spot headers of interest */ + + while ((name = string_nextinlist(&hnames, &sep, NULL, 0))) + { + int len = Ustrlen(name); + if (strncasecmp(CCS htext, CCS name, len) == 0) + { + /* If too long, fold line in h= field */ + + if (col + len > 78) g = string_catn(g, US"\r\n\t ", 5), col = 3; + + /* Add name to h= list */ + + g = string_catn(g, name, len); + g = string_catn(g, US":", 1); + col += len + 1; + + /* Accumulate header for hashing/signing */ + + hdata = string_cat(hdata, + arc_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */ + break; + } + } + } + +/* Lose the last colon from the h= list */ + +gstring_trim_trailing(g, ':'); + +g = string_catn(g, US";\r\n\tb=;", 7); + +/* Include the pseudo-header in the accumulation */ + +s = arc_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE); +hdata = string_cat(hdata, s); + +/* Calculate the signature from the accumulation */ +/*XXX does that need further relaxation? there are spaces embedded in the b= strings! */ + +if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AMS")) + return NULL; + +/* Lose the trailing semicolon from the psuedo-header, and append the signature +(folded over lines) and termination to complete it. */ + +gstring_trim(g, 1); +g = arc_sign_append_sig(g, &sig); + +h->slen = g->ptr - ams_off; +h->text = g->s + ams_off; +al->complete = h; +ctx->arcset_chain_last->hdr_ams = al; + +DEBUG(D_transport) debug_printf("ARC: AMS '%.*s'\n", h->slen - 2, h->text); +return g; +} + + + +/* Look for an arc= result in an A-R header blob. We know that its data +happens to be a NUL-term string. */ + +static uschar * +arc_ar_cv_status(blob * ar) +{ +const uschar * resinfo = ar->data; +int sep = ';'; +uschar * methodspec, * s; + +while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0))) + if (Ustrncmp(methodspec, US"arc=", 4) == 0) + { + uschar c; + for (s = methodspec += 4; + (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++; + return string_copyn(methodspec, s - methodspec); + } +return US"none"; +} + + + +/* Build the AS header and prepend it */ + +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, unsigned options) +{ +gstring * arcset; +uschar * status = arc_ar_cv_status(ar); +arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED); +header_line * h = (header_line *)(al+1); +uschar * badline_str; + +gstring * hdata = NULL; +const blob as_h = {.data = US"sha256", .len = 6}; /*XXX hardwired */ +int hashtype = arc_dkim_hashname_blob_to_type(&as_h); +blob sig; + +/* +- Generate AS + - no body coverage + - no h= tag; implicit coverage + - arc status from A-R + - if fail: + - coverage is just the new ARC set + including self (but with an empty b= in self) + - if non-fail: + - all ARC set headers, set-number order, aar then ams then as, + including self (but with an empty b= in self) +*/ +DEBUG(D_transport) debug_printf("ARC: building AS for status '%s'\n", status); + +/* Construct the AS except for the signature */ + +arcset = string_append(NULL, 9, + ARC_HDR_AS, + US" i=", string_sprintf("%d", instance), + US"; cv=", status, + US"; a=rsa-sha256; d=", identity, /*XXX hardwired */ + US"; s=", selector); /*XXX same as AMS */ +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; +h->text = arcset->s; +al->complete = h; +ctx->arcset_chain_last->hdr_as = al; + +/* For any but "fail" chain-verify status, walk the entire chain in order by +instance. For fail, only the new arc-set. Accumulate the elements walked. */ + +for (arc_set * as = Ustrcmp(status, US"fail") == 0 + ? ctx->arcset_chain_last : ctx->arcset_chain; + as; as = as->next) + { + arc_line * l; + /* Accumulate AAR then AMS then AS. Relaxed canonicalisation + is required per standard. */ + + badline_str = US"aar"; + if (!(l = as->hdr_aar)) goto badline; + h = l->complete; + hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, TRUE)); + badline_str = US"ams"; + if (!(l = as->hdr_ams)) goto badline; + h = l->complete; + hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, TRUE)); + badline_str = US"as"; + if (!(l = as->hdr_as)) goto badline; + h = l->complete; + hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, !!as->next)); + } + +/* Calculate the signature from the accumulation */ + +if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AS")) + return NULL; + +/* Lose the trailing semicolon */ +arcset->ptr--; +arcset = arc_sign_append_sig(arcset, &sig); +DEBUG(D_transport) debug_printf("ARC: AS '%.*s'\n", arcset->ptr - 2, arcset->s); + +/* Finally, append the AMS and AAR to the new AS */ + +return string_catn(arcset, arcset_interim->s, arcset_interim->ptr); + +badline: + DEBUG(D_transport) + debug_printf("ARC: while building AS, missing %s in chain\n", badline_str); + return NULL; +} + + +/**************************************/ + +static pdkim_bodyhash * +arc_ams_setup_sign_bodyhash(void) +{ +blob canon = {.data = US"relaxed", .len = 7}; /*XXX hardwired */ +blob hash = {.data = US"sha256", .len = 6}; /*XXX hardwired */ + +DEBUG(D_transport) debug_printf("ARC: requesting bodyhash\n"); + +return arc_set_bodyhash(TRUE, &canon, &hash, -1); +} + + + +/* Module API: initilise, and set up a bodyhash for AMS */ + +static void +arc_sign_init(void) +{ +blob canon = {.data = US"relaxed", .len = 7}; /*XXX hardwired */ +blob hash = {.data = US"sha256", .len = 6}; /*XXX hardwired */ + +memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx)); +headers_rlist = NULL; + +(void) arc_ams_setup_sign_bodyhash(); +} + + + +/* 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. + +We have to take the feed from DKIM as, in the transport-filter case, the +headers are not in memory at the time of the call to arc_sign(). + +Take a copy of the header and construct a reverse-order list. +Also parse ARC-chain headers and build the chain struct, retaining pointers +into the copies. +*/ + +static const uschar * +arc_header_sign_feed(gstring * g) +{ +uschar * s = string_copy_from_gstring(g); +headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr); +return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE); +} + + + +/* Per RFCs 6376, 7489 the only allowed chars in either an ADMD id +or a selector are ALPHA/DIGGIT/'-'/'.' + +Check, to help catch misconfigurations such as a missing selector +element in the arc_sign list. +*/ + +static BOOL +arc_valid_id(const uschar * s) +{ +for (uschar c; c = *s++; ) + if (!isalnum(c) && c != '-' && c != '.') return FALSE; +return TRUE; +} + + + +/* Module API: ARC signing. + +Called from the smtp transport, if the arc_sign option is set. +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. + Optional fourth element: comma-sep list of options. + Already expanded + sigheaders Any signature headers already generated, eg. by DKIM, or NULL + errstr Error string + +Return value + Set of headers to prepend to the message, including the supplied sigheaders + but not the plainheaders. +*/ + +static gstring * +arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr) +{ +const uschar * identity, * selector, * privkey, * opts, * s; +unsigned options = 0; +int sep = 0; +header_line * headers; +hdr_rlist * rheaders; +blob ar; +int instance; +gstring * g = NULL; +pdkim_bodyhash * b; + +expire = now = 0; + +/* Parse the signing specification */ + +if (!(identity = string_nextinlist(&signspec, &sep, NULL, 0)) || !*identity) + { s = US"identity"; goto bad_arg_ret; } +if (!(selector = string_nextinlist(&signspec, &sep, NULL, 0)) || !*selector) + { s = US"selector"; goto bad_arg_ret; } +if (!(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey) + { s = US"privkey"; goto bad_arg_ret; } +if (!arc_valid_id(identity)) + { s = US"identity"; goto bad_arg_ret; } +if (!arc_valid_id(selector)) + { s = US"selector"; goto bad_arg_ret; } +if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey))) + goto ret_sigheaders; + +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(CS ++s))) + expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA; + if (!now) now = time(NULL); + expire += now; + } + else + expire = (time_t)atol(CS 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. +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 = &headers_rlist; *rp; ) rp = &(*rp)->prev; + *rp = rheaders; + } + +/* 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; + 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, "ARC: no Authentication-Results header for signing"); + goto ret_sigheaders; + } + +/* 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 +*/ + +g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar); + +/* +- Generate AMS + - Looks fairly like a DKIM sig + - Cover all DKIM sig headers as well as the usuals + - ? oversigning? + - Covers the data + - we must have requested a suitable bodyhash previously + [done in arc_sign_init()] +*/ + +b = arc_ams_setup_sign_bodyhash(); +g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector, + &b->bh, headers_rlist, privkey, options); + +/* +- Generate AS + - no body coverage + - no h= tag; implicit coverage + - arc status from A-R + - if fail: + - coverage is just the new ARC set + including self (but with an empty b= in self) + - if non-fail: + - all ARC set headers, set-number order, aar then ams then as, + including self (but with an empty b= in self) +*/ + +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. */ + +if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr); + +out: + if (!g) return string_get(1); + (void) string_from_gstring(g); + gstring_release_unused(g); + return g; + + +bad_arg_ret: + log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)", s); +ret_sigheaders: + g = sigheaders; + goto out; +} + + +/******************************************************************************/ + +/* Check to see if the line is an AMS and if so, set up to validate it. +Called from the DKIM input processing. This must be done now as the message +body data is hashed during input. + +We call the DKIM code to request a body-hash; it has the facility already +and the hash parameters might be common with other requests. +*/ + +static const uschar * +arc_header_vfy_feed(gstring * g) +{ +header_line h; +arc_line al; +pdkim_bodyhash * b; +uschar * errstr; + +if (strncmpic(ARC_HDR_AMS, g->s, ARC_HDRLEN_AMS) != 0) return US"not AMS"; + +DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n"); +/* Parse the AMS header */ + +memset(&al, 0, sizeof(arc_line)); +h.next = NULL; +h.slen = len_string_from_gstring(g, &h.text); +if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, le_all))) + { + DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr); + goto badline; + } + +if (!al.a_hash.data) + { + DEBUG(D_acl) debug_printf("ARC: no a_hash from '%.*s'\n", h.slen, h.text); + goto badline; + } + +/* defaults */ +if (!al.c.data) + { + al.c_body.data = US"simple"; al.c_body.len = 6; + al.c_head = al.c_body; + } + +/* Ask the dkim code to calc a bodyhash with those specs */ + +if (!(b = arc_ams_setup_vfy_bodyhash(&al))) + return US"dkim hash setup fail"; + +/* Discard the reference; search again at verify time, knowing that one +should have been created here. */ + +return NULL; + +badline: + return US"line parsing error"; +} + + + +/* Module API: A header line has been identified by DKIM processing; +feed it to ARC processing. + +Arguments: + g Header line + is_vfy TRUE for verify mode or FALSE for signing mode + +Return: + NULL for success, or an error string (probably unused) +*/ + +static const uschar * +arc_header_feed(gstring * g, BOOL is_vfy) +{ +return is_vfy ? arc_header_vfy_feed(g) : arc_header_sign_feed(g); +} + + + +/******************************************************************************/ + +/* Construct the list of domains from the ARC chain after validation */ + +const 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); + } +if (!g) return US""; +return string_from_gstring(g); +} + + +/* Construct an Authentication-Results header portion, for the ARC module */ + +gstring * +authres_arc(gstring * g) +{ +if (arc_state) + { + int start = 0; /* Compiler quietening */ + DEBUG(D_acl) start = gstring_length(g); + + g = string_append(g, 2, US";\n\tarc=", arc_state); + if (arc_received_instance > 0) + { + g = string_fmt_append(g, " (i=%d)", arc_received_instance); + if (arc_state_reason) + g = string_append(g, 3, US"(", arc_state_reason, US")"); + + g = string_fmt_append(g, " header.s=%b arc.oldest-pass=%d", + &arc_received->hdr_ams->s, + arc_oldest_pass); + + if (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:\tauthres '%.*s'\n", + gstring_length(g) - start - 3, g->s + start + 3); + } +else + DEBUG(D_acl) debug_printf("ARC:\tno authres\n"); +return g; +} + + +# ifdef SUPPORT_DMARC + +/* Module API: obtain ARC info for DMARC history. +Arguments: + gp pointer for return of arcset info string +Return: + status string, or NULL if none +*/ + +static const uschar * +arc_arcset_string(gstring ** gp) +{ +if (arc_state) + { + gstring * g = NULL; + + /*XXX would we prefer this backwards? */ + for (arc_set * as = arc_verify_ctx.arcset_chain; as; as = as->next) + { + arc_line * line = as->hdr_as; + if (line) + { + g = string_append_listele_fmt(g, ',', " (\"i\":%u" /*)*/ + ", \"d\":\"%#b\"" + ", \"s\":\"%#b\"", + as->instance, &line->d, &line->s); + + if ((line = as->hdr_aar)) + { + blob * ip = &line->ip; + if (ip->data && ip->len) + g = string_fmt_append(g, ", \"ip\":\"%#b\"", ip); + } + /*(*/ + g = string_catn(g, US")", 1); + } + } + *gp = g; + } +return arc_state; +} +# endif + + +/******************************************************************************/ +/* Module API */ + +static void * arc_functions[] = { + [ARC_VERIFY] = acl_verify_arc, + [ARC_HEADER_FEED] = arc_header_feed, + [ARC_STATE_IS_PASS] = arc_is_pass, + [ARC_SIGN_INIT] = arc_sign_init, + [ARC_SIGN] = arc_sign, +# ifdef SUPPORT_DMARC + [ARC_ARCSET_INFO] = arc_arcset_string, +# endif +}; + +static var_entry arc_variables[] = { + { "arc_domains", vtype_string_func, (void *) &fn_arc_domains }, + { "arc_oldest_pass", vtype_int, &arc_oldest_pass }, + { "arc_state", vtype_stringptr, &arc_state }, + { "arc_state_reason", vtype_stringptr, &arc_state_reason }, +}; + +misc_module_info arc_module_info = +{ + .name = US"arc", +# if SUPPORT_SPF==2 + .dyn_magic = MISC_MODULE_MAGIC, +# endif + .init = arc_init, + .smtp_reset = arc_smtp_reset, + .authres = authres_arc, + + .functions = arc_functions, + .functions_count = nelem(arc_functions), + + .variables = arc_variables, + .variables_count = nelem(arc_variables), +}; + +# endif /* DISABLE_DKIM */ +#endif /* EXPERIMENTAL_ARC */ +/* vi: aw ai sw=2 + */ diff --git a/src/src/miscmods/arc_api.h b/src/src/miscmods/arc_api.h new file mode 100644 index 000000000..cf24a4cb3 --- /dev/null +++ b/src/src/miscmods/arc_api.h @@ -0,0 +1,19 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2024 */ +/* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* API definitions for the arcmodule */ + + +/* Function table entry numbers */ + +#define ARC_VERIFY 0 +#define ARC_HEADER_FEED 1 +#define ARC_STATE_IS_PASS 2 +#define ARC_SIGN_INIT 3 +#define ARC_SIGN 4 +#define ARC_ARCSET_INFO 5 diff --git a/src/src/miscmods/dkim_transport.c b/src/src/miscmods/dkim_transport.c index 0500da2be..e2d1705e3 100644 --- a/src/src/miscmods/dkim_transport.c +++ b/src/src/miscmods/dkim_transport.c @@ -109,6 +109,32 @@ return TRUE; +/* Prepend ARC-signing headers to given set of headers + +Arguments: + 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 + +Return value + Set of headers to prepend to the message, including the supplied sigheaders + but not the plainheaders. +*/ + +static gstring * +dkt_arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr_p) +{ +const misc_module_info * mi = misc_mod_findonly(US"arc"); +typedef gstring * (*fn_t)(const uschar *, gstring *, uschar **); +if (mi) + return (((fn_t *) mi->functions)[ARC_SIGN]) (signspec, sigheaders, errstr_p); +*errstr_p = US"failed to find arc module"; +return NULL; +} + + /* This function is a wrapper around transport_write_message(). It is only called from the smtp transport if DKIM or Domainkeys support @@ -153,11 +179,6 @@ if (!rc) return FALSE; /* Get signatures for headers plus spool data file */ -#ifdef EXPERIMENTAL_ARC -arc_sign_init(); /*XXX perhaps move this call back to the smtp tpt - around where it currently calls arc_ams_setup_sign_bodyhash() ? */ -#endif - /* The dotstuffed status of the datafile depends on whether it was stored in wireformat. */ @@ -174,7 +195,7 @@ if (!(dkim_signature = dkim_exim_sign(deliver_datafile, if (dkim->arc_signspec) /* Prepend ARC headers */ { uschar * e = NULL; - if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, &e))) + if (!(dkim_signature = dkt_arc_sign(dkim->arc_signspec, dkim_signature, &e))) { *err = e; return FALSE; @@ -275,10 +296,6 @@ if (!rc) goto CLEANUP; } -#ifdef EXPERIMENTAL_ARC -arc_sign_init(); -#endif - /* Feed the file to the goats^W DKIM lib. At this point the dotstuffed status of the file depends on the output of transport_write_message() just above, which should be the result of the end_dot flag in tctx->options. */ @@ -299,7 +316,8 @@ else #ifdef EXPERIMENTAL_ARC if (dkim->arc_signspec) /* Prepend ARC headers */ { - if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, USS err))) + if (!(dkim_signature = dkt_arc_sign(dkim->arc_signspec, dkim_signature, + USS err))) goto CLEANUP; dlen = dkim_signature->ptr; } diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c index d977d29fe..e192bda1b 100644 --- a/src/src/miscmods/dmarc.c +++ b/src/src/miscmods/dmarc.c @@ -360,7 +360,32 @@ g = string_fmt_append(g, "align_dkim %d\nalign_spf %d\naction %d\n", #if DMARC_API >= 100400 # ifdef EXPERIMENTAL_ARC -g = arc_dmarc_hist_append(g); + { + const misc_module_info * mi = misc_mod_findonly(US"arc"); + const uschar * s; + gstring * g2 = NULL; + typedef const uschar * (*fn_t)(gstring **); + + if (mi && (s = (((fn_t *) mi->functions)[ARC_ARCSET_INFO]) (&g2))) + { + int i = Ustrcmp(s, "pass") == 0 ? ARES_RESULT_PASS + : Ustrcmp(s, "fail") == 0 ? ARES_RESULT_FAIL + : ARES_RESULT_UNKNOWN; + + g = string_fmt_append(g, "arc %d\n" + "arc_policy %d json[%#Y ]\n", + i, + i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS + : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL + : DMARC_ARC_POLICY_RESULT_UNUSED, + g2 + ); + } + else + string_fmt_append(g, "arc %d\narc_policy %d json:[]\n", + ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED); + } + # else g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n", ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED); diff --git a/src/src/miscmods/pdkim/pdkim.c b/src/src/miscmods/pdkim/pdkim.c index cdbdfc5e0..7c2f34217 100644 --- a/src/src/miscmods/pdkim/pdkim.c +++ b/src/src/miscmods/pdkim/pdkim.c @@ -935,13 +935,19 @@ return; static int pdkim_header_complete(pdkim_ctx * ctx) { -if (ctx->cur_header->ptr > 1) - gstring_trim_trailing(ctx->cur_header, '\r'); -(void) string_from_gstring(ctx->cur_header); +gstring * g = ctx->cur_header; +const misc_module_info * mi; +typedef const uschar * (*fn_t)(gstring *, BOOL); + +if (gstring_length(g) > 1) + gstring_trim_trailing(g, '\r'); +(void) string_from_gstring(g); #ifdef EXPERIMENTAL_ARC -/* Feed the header line to ARC processing */ -(void) arc_header_feed(ctx->cur_header, !(ctx->flags & PDKIM_MODE_SIGN)); +/* Feed the header line also to ARC processing */ +if ((mi = misc_mod_findonly(US"arc"))) + (((fn_t *) mi->functions)[ARC_HEADER_FEED]) + (g, !(ctx->flags & PDKIM_MODE_SIGN)); #endif if (++ctx->num_headers > PDKIM_MAX_HEADERS) goto BAIL; @@ -951,7 +957,7 @@ if (ctx->flags & PDKIM_MODE_SIGN) for (pdkim_signature * sig = ctx->sig; sig; sig = sig->next) /* Traverse all signatures */ /* Add header to the signed headers list (in reverse order) */ - sig->headers = pdkim_prepend_stringlist(sig->headers, ctx->cur_header->s); + sig->headers = pdkim_prepend_stringlist(sig->headers, g->s); /* VERIFICATION ----------------------------------------------------------- */ /* DKIM-Signature: headers are added to the verification list */ @@ -959,9 +965,9 @@ else { #ifdef notdef DEBUG(D_acl) debug_printf("DKIM >> raw hdr: %.*Z\n", - ctx->cur_head->ptr, CUS ctx->cur_header->s); + ctx->cur_head->ptr, CUS g->s); #endif - if (strncasecmp(CCS ctx->cur_header->s, + if (strncasecmp(CCS g->s, DKIM_SIGNATURE_HEADERNAME, Ustrlen(DKIM_SIGNATURE_HEADERNAME)) == 0) { @@ -973,7 +979,7 @@ else DEBUG(D_acl) debug_printf( "DKIM >> Found sig, trying to parse >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); - sig = pdkim_parse_sig_header(ctx, ctx->cur_header->s); + sig = pdkim_parse_sig_header(ctx, g->s); if (!(last_sig = ctx->sig)) ctx->sig = sig; @@ -985,18 +991,18 @@ else if (dkim_collect_input && --dkim_collect_input == 0) { - ctx->headers = pdkim_prepend_stringlist(ctx->headers, ctx->cur_header->s); - ctx->cur_header->s[ctx->cur_header->ptr = 0] = '\0'; + ctx->headers = pdkim_prepend_stringlist(ctx->headers, g->s); + g->s[g->ptr = 0] = '\0'; return PDKIM_ERR_EXCESS_SIGS; } } /* all headers are stored for signature verification */ - ctx->headers = pdkim_prepend_stringlist(ctx->headers, ctx->cur_header->s); + ctx->headers = pdkim_prepend_stringlist(ctx->headers, g->s); } BAIL: -ctx->cur_header->s[ctx->cur_header->ptr = 0] = '\0'; /* leave buffer for reuse */ +g->s[g->ptr = 0] = '\0'; /* leave buffer for reuse */ return PDKIM_OK; } diff --git a/src/src/miscmods/spf_api.h b/src/src/miscmods/spf_api.h index 117a7ae6a..0e1907d9a 100644 --- a/src/src/miscmods/spf_api.h +++ b/src/src/miscmods/spf_api.h @@ -12,7 +12,7 @@ /* Function table entry numbers */ #define SPF_PROCESS 0 -#define SPF_GET_RESPONSE 2 -#define SPF_OPEN 3 -#define SPF_CLOSE 4 -#define SPF_FIND 5 +#define SPF_GET_RESPONSE 1 +#define SPF_OPEN 2 +#define SPF_CLOSE 3 +#define SPF_FIND 4 diff --git a/src/src/receive.c b/src/src/receive.c index 541e9320d..ae4e1ff7e 100644 --- a/src/src/receive.c +++ b/src/src/receive.c @@ -4135,11 +4135,16 @@ if (LOGGING(dkim)) typedef gstring * (*fn_t)(gstring *); if (mi) g = (((fn_t *) mi->functions)[DKIM_VDOM_FIRSTPASS]) (g); - } + # ifdef EXPERIMENTAL_ARC -if (LOGGING(dkim) && arc_state && Ustrcmp(arc_state, "pass") == 0) - g = string_catn(g, US" ARC", 4); + { + mi = misc_mod_findonly(US"arc"); + typedef BOOL (*fn_t)(void); + if (mi && (((fn_t *) mi->functions)[ARC_STATE_IS_PASS]) ()) + g = string_catn(g, US" ARC", 4); + } # endif + } #endif if (LOGGING(receive_time)) diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c index e75894850..e76790fea 100644 --- a/src/src/smtp_in.c +++ b/src/src/smtp_in.c @@ -1701,10 +1701,7 @@ bmi_run = 0; bmi_verdicts = NULL; #endif dnslist_domain = dnslist_matched = NULL; -#ifdef EXPERIMENTAL_ARC -arc_state = arc_state_reason = NULL; -arc_received_instance = 0; -#endif + dsn_ret = 0; dsn_envid = NULL; deliver_host = deliver_host_address = NULL; /* Can be set by ACL */ diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c index 36b5e61fc..594b42e1f 100644 --- a/src/src/transports/smtp.c +++ b/src/src/transports/smtp.c @@ -32,7 +32,7 @@ optionlist smtp_transport_options[] = { LOFF(address_retry_include_sender) }, { "allow_localhost", opt_bool, LOFF(allow_localhost) }, #ifdef EXPERIMENTAL_ARC - { "arc_sign", opt_stringptr, LOFF(arc_sign) }, + { "arc_sign", opt_stringptr, LOFF(arc_sign) }, #endif { "authenticated_sender", opt_stringptr, LOFF(authenticated_sender) }, { "authenticated_sender_force", opt_bool, LOFF(authenticated_sender_force) }, @@ -4104,8 +4104,8 @@ else #ifndef DISABLE_DKIM { - typedef void (*fn_t)(void); misc_module_info * mi; + # ifdef MEASURE_TIMING struct timeval t0; gettimeofday(&t0, NULL); @@ -4113,27 +4113,30 @@ else if ((mi = misc_mod_find(US"dkim", NULL))) { + typedef void (*fn_t)(void); (((fn_t *) mi->functions)[DKIM_TRANSPORT_INIT]) (); # ifdef EXPERIMENTAL_ARC - uschar * s = ob->arc_sign; - if (s) { - if (!(ob->dkim.arc_signspec = s = expand_string(s))) - { - if (!f.expand_string_forcedfail) + uschar * s = ob->arc_sign; + if (s) + if (!(ob->dkim.arc_signspec = s = expand_string(s))) { - message = US"failed to expand arc_sign"; - sx->ok = FALSE; - goto SEND_FAILED; + if (!f.expand_string_forcedfail) + { + message = US"failed to expand arc_sign"; + sx->ok = FALSE; + goto SEND_FAILED; + } + } + else if (*s && (mi = misc_mod_find(US"arc", NULL))) + { + typedef void (*fn_t)(void); + (((fn_t *) mi->functions)[ARC_SIGN_INIT]) (); + + /* Ask dkim code to hash the body for ARC */ + ob->dkim.force_bodyhash = TRUE; } - } - else if (*s) - { - /* Ask dkim code to hash the body for ARC */ - (void) arc_ams_setup_sign_bodyhash(); - ob->dkim.force_bodyhash = TRUE; - } } # endif /*ARC*/ }