From: Jeremy Harris Date: Wed, 4 Sep 2024 20:46:26 +0000 (+0100) Subject: dkim dynamic module X-Git-Url: https://git.exim.org/exim.git/commitdiff_plain/9a0f997bac85d8f234238162f3cee4524b6f989c dkim dynamic module --- diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 640bd58cd..1189ce3f3 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 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 and DMARC 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/src/OS/Makefile-Base b/src/OS/Makefile-Base index 591b4261c..12319967e 100644 --- a/src/OS/Makefile-Base +++ b/src/OS/Makefile-Base @@ -221,15 +221,15 @@ macro-spa.o : auths/spa.c macro-authtls.o: auths/tls.c @echo "$(CC) -DMACRO_PREDEF auths/tls.c" $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ auths/tls.c -macro-dkim.o: dkim.c - @echo "$(CC) -DMACRO_PREDEF dkim.c" - $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ dkim.c +macro-dkim.o: miscmods/dkim.c + @echo "$(CC) -DMACRO_PREDEF miscmods/dkim.c" + $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ miscmods/dkim.c macro-malware.o: malware.c @echo "$(CC) -DMACRO_PREDEF malware.c" $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ malware.c -macro-signing.o: pdkim/signing.c - @echo "$(CC) -DMACRO_PREDEF pdkim/signing.c" - $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ pdkim/signing.c +macro-signing.o: miscmods/signing.c + @echo "$(CC) -DMACRO_PREDEF miscmods/signing.c" + $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ miscmods/signing.c macro_predef: $(OBJ_MACRO) @echo "$(LNCC) -o $@" @@ -244,7 +244,7 @@ macro.c: macro_predef # problem, but it does no harm. Other make programs will just ignore this. .PHONY: all config utils \ - buildauths buildlookups buildpdkim buildrouters \ + buildauths buildlookups buildrouters \ buildtransports buildmisc dynmodules checklocalmake clean @@ -515,7 +515,7 @@ OBJ_AUTHS = call_pam.o call_pwcheck.o call_radius.o check_serv_cond.o \ OBJ_EXIM = acl.o base64.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o \ directory.o dns.o drtables.o enq.o exim.o expand.o filter.o \ - filtertest.o globals.o dkim.o dkim_transport.o dnsbl.o hash.o \ + filtertest.o globals.o dnsbl.o hash.o \ header.o host.o host_address.o ip.o log.o lss.o match.o md5.o moan.o \ os.o parse.o priv.o proxy.o queue.o \ rda.o readconf.o receive.o retry.o rewrite.o rfc2047.o regex_cache.o \ @@ -526,13 +526,13 @@ OBJ_EXIM = acl.o base64.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o \ local_scan.o $(EXIM_PERL) $(OBJ_WITH_CONTENT_SCAN) \ $(OBJ_EXPERIMENTAL) -exim: buildlookups buildauths pdkim/pdkim.a \ +exim: buildlookups buildauths \ buildrouters buildtransports buildmisc \ $(OBJ_EXIM) version.o @echo "$(LNCC) -o exim" $(FE)$(PURIFY) $(LNCC) -o exim $(LFLAGS) $(OBJ_EXIM) version.o \ routers/routers.a transports/transports.a lookups/lookups.a \ - auths/auths.a pdkim/pdkim.a miscmods/miscmods.a \ + auths/auths.a miscmods/miscmods.a \ $(LIBRESOLV) $(LIBS) $(LIBS_EXIM) $(IPV6_LIBS) $(EXTRALIBS) \ $(EXTRALIBS_EXIM) $(DBMLIB) $(LOOKUP_LIBS) $(AUTH_LIBS) \ $(PERL_LIBS) $(TLS_LIBS) $(PCRE_LIBS) $(LDFLAGS) @@ -685,6 +685,7 @@ HDRS = blob.h \ hintsdb/hints_tdb.h \ local_scan.h \ macros.h \ + miscmods/dkim_api.h \ miscmods/dmarc_api.h \ miscmods/spf_api.h \ mytypes.h \ @@ -706,6 +707,7 @@ PHDRS = ../config.h \ ../hintsdb/hints_tdb.h \ ../local_scan.h \ ../macros.h \ + ../miscmods/dkim_api.h \ ../miscmods/dmarc_api.h \ ../miscmods/spf_api.h \ ../mytypes.h \ @@ -886,8 +888,6 @@ transport.o: $(HDRS) transport.c tree.o: $(HDRS) tree.c verify.o: $(HDRS) transports/smtp.h verify.c xtextencode.o: $(HDRS) xtextencode.c -dkim.o: $(HDRS) pdkim/pdkim.h dkim.c -dkim_transport.o: $(HDRS) dkim_transport.c # Dependencies for WITH_CONTENT_SCAN modules @@ -900,7 +900,7 @@ spool_mbox.o: $(HDRS) spool_mbox.c # Dependencies for EXPERIMENTAL_* modules -arc.o: $(HDRS) pdkim/pdkim.h arc.c +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 @@ -1065,15 +1065,6 @@ buildauths: config INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)" @echo " " -# The PDKIM library - -buildpdkim: pdkim/pdkim.a -pdkim/pdkim.a: config - @cd pdkim && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \ - FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \ - INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)" - @echo " " - buildmisc: config @cd miscmods && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) \ CC="$(CC)" CFLAGS="$(CFLAGS)" \ diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile index c3019f846..12f0ddd9c 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 SPF DMARC + miscmods SUPPORT _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 f657abd5b..a6521a95e 100755 --- a/src/scripts/MakeLinks +++ b/src/scripts/MakeLinks @@ -90,13 +90,20 @@ done cd .. # miscellaneous modules +# Note that the file in the miscmods/pdkim/ source subdir get linked to the +# destination miscmods/ dir d="miscmods" mkdir $d cd $d # Makefile is generated -for f in dmarc.c dmarc.h dmarc_api.h dummy.c spf.c spf.h spf_api.h +for f in dummy.c \ + 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 \ + dmarc.c dmarc.h dmarc_api.h \ + spf.c spf.h spf_api.h do - ln -s ../../src/$d/$f $f + ln -s ../../src/$d/$f `basename $f` done cd .. @@ -110,17 +117,6 @@ do done cd .. -# Likewise for the code for the PDKIM library -d="pdkim" -mkdir $d -cd $d -for f in README Makefile crypt_ver.h pdkim.c \ - pdkim.h hash.c hash.h signing.c signing.h blob.h -do - ln -s ../../src/$d/$f $f -done -cd .. - # The basic source files for Exim and utilities. NB local_scan.h gets linked, # but local_scan.c does not, because its location is taken from the build-time # configuration. Likewise for the os.c file, which gets build dynamically. @@ -140,7 +136,6 @@ for f in blob.h dbfunctions.h exim.h functions.h globals.h \ string.c tls.c tlscert-gnu.c tlscert-openssl.c tls-cipher-stdname.c \ tls-gnu.c tls-openssl.c \ tod.c transport.c tree.c verify.c version.c xtextencode.c \ - dkim.c dkim.h dkim_transport.c \ valgrind.h memcheck.h \ macro_predef.c macro_predef.h do @@ -155,7 +150,7 @@ done # EXPERIMENTAL_* for f in arc.c bmi_spam.c bmi_spam.h dcc.c dcc.h dane.c dane-openssl.c \ - danessl.h imap_utf7.c spf.c spf.h utf8.c xclient.c + danessl.h imap_utf7.c utf8.c xclient.c do ln -s ../src/$f $f done diff --git a/src/scripts/drivers-Makefile b/src/scripts/drivers-Makefile index 2dd958043..085eedbda 100755 --- a/src/scripts/drivers-Makefile +++ b/src/scripts/drivers-Makefile @@ -95,13 +95,25 @@ fi # command-line, not just check the Makefile. want_dynamic() { - local dyn_name="$1" + local dyn_name="${1#_}" local re="(${classdef}|EXPERIMENTAL)_${dyn_name}[ $tab]*=[ $tab]*2" + #XXX Solaris does not support -E on grep. Must use egrep. env | grep -E -q "^$re" if [ $? -eq 0 ]; then return 0; fi grep -E -q "^[ $tab]*$re" "$defs_source" } +want_not_disabled() { + local want_name="${1#_}" + [ "$local_want_name" = "$1" ] && return 0; + local re="DISABLED_${want_name}[ $tab]*=[ $tab]*." + env | grep -E -q "^$re" + [ $? -ne 0 ] && return 0 + grep -E -q "^[ $tab]*$re" "$defs_source" + [ $? -ne 0 ] && return 0 + return 1 +} + want_at_all() { local want_name="$1" local re="(${classdef}|EXPERIMENTAL)_${want_name}[ $tab]*=[ $tab]*." @@ -135,20 +147,21 @@ emit_module_rule() { echo >&2 "Missing CFLAGS_DYNAMIC prevents building dynamic $name" exit 1 fi - MODS="${MODS} ${mod_name}.so" + MODS="${MODS} ${mod_name#_}.so" grep "^${classdef}_${name}_PC" "$defs_source" 1>&2 pkgconf=$(grep "^${classdef}_${name}_PC" "$defs_source") if [ $? -eq 0 ]; then pkgconf=$(echo $pkgconf | sed 's/^.*= *//') - echo "${classdef}_${mod_name}_INCLUDE = $(pkg-config --cflags $pkgconf)" - echo "${classdef}_${mod_name}_LIBS = $(pkg-config --libs $pkgconf)" + echo "${classdef}_${mod_name#_}_INCLUDE = $(pkg-config --cflags $pkgconf)" + echo "${classdef}_${mod_name#_}_LIBS = $(pkg-config --libs $pkgconf)" else grep "^${classdef}_${name}_" "$defs_source" - echo "${classdef}_${mod_name}_INCLUDE = \$(${classdef}_${name}_INCLUDE)" - echo "${classdef}_${mod_name}_LIBS = \$(${classdef}_${name}_LIBS)" + echo "${classdef}_${mod_name#_}_INCLUDE = \$(${classdef}_${name}_INCLUDE)" + echo "${classdef}_${mod_name#_}_LIBS = \$(${classdef}_${name}_LIBS)" fi - elif want_at_all "$name" - then + elif want_not_disabled "$name"; then + OBJ="${OBJ} ${mod_name#_}.o" + elif want_at_all "$name"; then OBJ="${OBJ} ${mod_name}.o" fi } diff --git a/src/src/EDITME b/src/src/EDITME index 35c497697..9d458842a 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -586,6 +586,10 @@ DISABLE_MAL_MKS=yes # turned on by default. See the spec for information on conditionally # disabling it. To disable the inclusion of the entire feature, set # DISABLE_DKIM to "yes" +# +# It is possible to build the support as a dynamic-load module. In addition +# to not defining DISABLE_DKIM, define SUPPORT_DKIM=2. The usual rules on +# defines for includes and libs apply. # DISABLE_DKIM=yes diff --git a/src/src/acl.c b/src/src/acl.c index 023ac2ff6..878278313 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -3934,23 +3934,19 @@ for (; cb; cb = cb->next) #ifndef DISABLE_DKIM case ACLC_DKIM_SIGNER: - if (dkim_cur_signer) - rc = match_isinlist(dkim_cur_signer, - &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); - else - rc = FAIL; - break; - case ACLC_DKIM_STATUS: - { /* return good for any match */ - const uschar * s = dkim_verify_status ? dkim_verify_status : US"none"; - int sep = 0; - for (uschar * ss; ss = string_nextinlist(&s, &sep, NULL, 0); ) - if ( (rc = match_isinlist(ss, &arg, - 0, NULL, NULL, MCL_STRING, TRUE, NULL)) - == OK) break; - } + /* See comment on ACLC_SPF wrt. coding issues */ + { + misc_module_info * mi = misc_mod_find(US"dkim", &log_message); + typedef int (*fn_t)(const uschar *); + rc = mi + ? (((fn_t *) mi->functions) + [cb->type == ACLC_DKIM_SIGNER + ? DKIM_SIGNER_ISINLIST + : DKIM_STATUS_LISTMATCH]) (arg) + : DEFER; break; + } #endif #ifdef SUPPORT_DMARC @@ -4183,11 +4179,19 @@ for (; cb; cb = cb->next) #endif ) store_pool = POOL_PERM; + #ifndef DISABLE_DKIM /* Overwriteable dkim result variables */ - if (Ustrcmp(cb->u.varname, "dkim_verify_status") == 0) - dkim_verify_status = string_copy(arg); - else if (Ustrcmp(cb->u.varname, "dkim_verify_reason") == 0) - dkim_verify_reason = string_copy(arg); + if ( Ustrcmp(cb->u.varname, "dkim_verify_status") == 0 + || Ustrcmp(cb->u.varname, "dkim_verify_reason") == 0 + ) + { + misc_module_info * mi = misc_mod_findonly(US"dkim"); + typedef void (*fn_t)(const uschar *, void *); + + if (mi) + (((fn_t *) mi->functions)[DKIM_SETVAR]) + (cb->u.varname, string_copy(arg)); + } else #endif acl_var_create(cb->u.varname)->data.ptr = string_copy(arg); @@ -4216,7 +4220,8 @@ for (; cb; cb = cb->next) case ACLC_SPF: case ACLC_SPF_GUESS: /* We have hardwired function-call numbers, and also prototypes for the - functions. We could do a function name table search for the number + functions. We could do a function name table search or (simpler) + a module include file with defines for the numbers but I can't see how to deal with prototypes. Is a K&R non-prototyped function still usable with today's compilers (but we would lose on type-checking)? We could macroize the typedef, and even the function diff --git a/src/src/arc.c b/src/src/arc.c index d24b61114..a065ca8e3 100644 --- a/src/src/arc.c +++ b/src/src/arc.c @@ -15,16 +15,13 @@ # else # include "functions.h" -# include "pdkim/pdkim.h" -# include "pdkim/signing.h" +# include "miscmods/pdkim.h" +# include "miscmods/signing.h" # ifdef SUPPORT_DMARC # include "miscmods/dmarc.h" # endif -extern pdkim_ctx * dkim_verify_ctx; -extern pdkim_ctx dkim_sign_ctx; - #define ARC_SIGN_OPT_TSTAMP BIT(0) #define ARC_SIGN_OPT_EXPIRE BIT(1) @@ -100,6 +97,8 @@ typedef enum line_extract { 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; @@ -132,6 +131,23 @@ 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; +} + /******************************************************************************/ @@ -586,6 +602,39 @@ 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. @@ -599,14 +648,12 @@ const uschar * hn; int sep = ':'; hdr_rlist * r; BOOL relaxed = Ustrncmp(US"relaxed", ams->c_head.data, ams->c_head.len) == 0; -int hashtype = pdkim_hashname_to_hashtype( - ams->a_hash.data, ams->a_hash.len); +hashmethod hm = arc_dkim_hashname_blob_to_method(&ams->a_hash); hctx hhash_ctx; const uschar * s; int len; -if ( hashtype == -1 - || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod)) +if (hm < 0 || !exim_sha_init(&hhash_ctx, hm)) { DEBUG(D_acl) debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); @@ -628,7 +675,7 @@ while ((hn = string_nextinlist(&headernames, &sep, NULL, 0))) && strncasecmp(CCS (s = r->h->text), CCS hn, Ustrlen(hn)) == 0 ) { - if (relaxed) s = pdkim_relax_header_n(s, r->h->slen, TRUE); + 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); @@ -640,7 +687,7 @@ while ((hn = string_nextinlist(&headernames, &sep, NULL, 0))) s = ams->rawsig_no_b_val.data, len = ams->rawsig_no_b_val.len; if (relaxed) - len = Ustrlen(s = pdkim_relax_header_n(s, len, FALSE)); + 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); @@ -653,33 +700,34 @@ return; -static pdkim_pubkey * -arc_line_to_pubkey(arc_line * al) +static blob * +arc_line_to_pubkey(arc_line * al, const uschar ** errstr) { -uschar * dns_txt; -pdkim_pubkey * p; - -if (!(dns_txt = dkim_exim_query_dns_txt(string_sprintf("%.*s._domainkey.%.*s", - (int)al->s.len, al->s.data, (int)al->d.len, al->d.data)))) - { - DEBUG(D_acl) debug_printf("pubkey dns lookup fail\n"); - return NULL; - } - -if ( !(p = pdkim_parse_pubkey_record(dns_txt)) - || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0) - ) +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)) { - DEBUG(D_acl) debug_printf("pubkey dns lookup format error\n"); + *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 (p->hashes) +if (hashes) { - const uschar * list = p->hashes, * ele; + const uschar * list = hashes, * ele; int sep = ':'; while ((ele = string_nextinlist(&list, &sep, NULL, 0))) @@ -687,11 +735,38 @@ if (p->hashes) if (!ele) { DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n", - p->hashes, (int)al->a.len, al->a.data); + hashes, (int)al->a.len, al->a.data); + *errstr = US"no usable sig for this pubkey hash list"; return NULL; } } -return p; +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); } @@ -700,22 +775,72 @@ return p; static pdkim_bodyhash * arc_ams_setup_vfy_bodyhash(arc_line * ams) { -int canon_head = -1, canon_body = -1; -long bodylen; - -if (!ams->c.data) ams->c.data = US"simple"; /* RFC 6376 (DKIM) default */ -pdkim_cstring_to_canons(ams->c.data, ams->c.len, &canon_head, &canon_body); -bodylen = ams->l.data - ? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) : -1; - -return pdkim_set_bodyhash(dkim_verify_ctx, - pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len), - canon_body, - bodylen); +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. */ @@ -725,12 +850,11 @@ arc_ams_verify(arc_ctx * ctx, arc_set * as) { arc_line * ams = as->hdr_ams; pdkim_bodyhash * b; -pdkim_pubkey * p; blob sighash; -blob hhash; -ev_ctx vctx; -int hashtype; +blob hhash_computed; +hashmethod hm; const uschar * errstr; +int rc; as->ams_verify_done = US"in-progress"; @@ -771,7 +895,7 @@ DEBUG(D_acl) /* We know the bh-tag blob is of a nul-term string, so safe as a string */ if ( !ams->bh.data - || (pdkim_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len) + || (arc_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len) || memcmp(sighash.data, b->bh.data, b->bh.len) != 0 ) { @@ -786,38 +910,21 @@ if ( !ams->bh.data DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance); -/* Get the public key from DNS */ - -if (!(p = arc_line_to_pubkey(ams))) - return as->ams_verify_done = arc_state_reason = US"pubkey problem"; - /* We know the b-tag blob is of a nul-term string, so safe as a string */ -pdkim_decode_base64(ams->b.data, &sighash); +arc_decode_base64(ams->b.data, &sighash); -arc_get_verify_hhash(ctx, ams, &hhash); +arc_get_verify_hhash(ctx, ams, &hhash_computed); -/* Setup the interface to the signing library */ - -if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx, NULL))) - { - DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr); - as->ams_verify_done = arc_state_reason = US"internal sigverify init error"; - return US"fail"; - } - -hashtype = pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len); -if (hashtype == -1) +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"; } -if ((errstr = exim_dkim_verify(&vctx, - pdkim_hashes[hashtype].exim_hashmethod, &hhash, &sighash))) - { - DEBUG(D_acl) debug_printf("ARC i=%d AMS verify %s\n", as->instance, errstr); - return 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; @@ -901,13 +1008,12 @@ arc_seal_verify(arc_ctx * ctx, arc_set * as) { arc_line * hdr_as = as->hdr_as; arc_set * as2; -int hashtype; +hashmethod hm; hctx hhash_ctx; blob hhash_computed; blob sighash; -ev_ctx vctx; -pdkim_pubkey * p; const uschar * errstr; +int rc; DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance); /* @@ -935,10 +1041,9 @@ if ( as->instance == 1 && !arc_cv_match(hdr_as, US"none") the ARC-Seal. */ -hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len); +hm = arc_dkim_hashname_blob_to_method(&hdr_as->a_hash); -if ( hashtype == -1 - || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod)) +if (hm < 0 || !exim_sha_init(&hhash_ctx, hm)) { DEBUG(D_acl) debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); @@ -967,7 +1072,8 @@ for (as2 = ctx->arcset_chain; al = as2->hdr_aar; if (!(s = al->relaxed)) - al->relaxed = s = pdkim_relax_header_n(al->complete->text, + /*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); @@ -975,7 +1081,8 @@ for (as2 = ctx->arcset_chain; al = as2->hdr_ams; if (!(s = al->relaxed)) - al->relaxed = s = pdkim_relax_header_n(al->complete->text, + /*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); @@ -983,10 +1090,12 @@ for (as2 = ctx->arcset_chain; al = as2->hdr_as; if (as2->instance == as->instance) - s = pdkim_relax_header_n(al->rawsig_no_b_val.data, + /*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)) - al->relaxed = s = pdkim_relax_header_n(al->complete->text, + /*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); @@ -1009,12 +1118,9 @@ DEBUG(D_acl) /* 6. Retrieve the public key identified by the "s" and "d" tags in the ARC-Seal, as described in Section 4.1.6. -*/ -if (!(p = arc_line_to_pubkey(hdr_as))) - return US"pubkey problem"; +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 @@ -1025,21 +1131,12 @@ if (!(p = arc_line_to_pubkey(hdr_as))) */ /* We know the b-tag blob is of a nul-term string, so safe as a string */ -pdkim_decode_base64(hdr_as->b.data, &sighash); - -if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx, NULL))) - { - DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr); - return US"fail"; - } +arc_decode_base64(hdr_as->b.data, &sighash); -if ((errstr = exim_dkim_verify(&vctx, - pdkim_hashes[hashtype].exim_hashmethod, - &hhash_computed, &sighash))) +rc = arc_sig_verify(as, hdr_as, hm, &hhash_computed, &sighash, US"AS", &errstr); +if (rc != OK) { - DEBUG(D_acl) - debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr); - arc_state_reason = US"seal sigverify error"; + if (rc == FAIL) arc_state_reason = US"seal sigverify error"; return US"fail"; } @@ -1076,12 +1173,6 @@ const uschar * res; memset(&arc_verify_ctx, 0, sizeof(arc_verify_ctx)); -if (!dkim_verify_ctx) - { - DEBUG(D_acl) debug_printf("ARC: no DKIM verify context\n"); - return NULL; - } - /* AS evaluation, per https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6 */ @@ -1285,10 +1376,13 @@ 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 : pdkim_hashes[hashtype].exim_hashmethod; + ? HASH_SHA2_512 + : arc_dkim_hashtype_to_method(hashtype); + blob hhash; -es_ctx sctx; const uschar * errstr; +typedef const uschar * (*fn_t) + (const blob *, hashmethod, const uschar *, blob *); DEBUG(D_transport) { @@ -1296,7 +1390,7 @@ DEBUG(D_transport) debug_printf("ARC: %s header data for signing:\n", why); debug_printf("%.*Z\n", hdata->ptr, hdata->s); - (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod); + (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); @@ -1305,7 +1399,7 @@ DEBUG(D_transport) if (FALSE /*need hash for Ed25519 or GCrypt signing*/ ) { hctx hhash_ctx; - (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod); + (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); } @@ -1315,8 +1409,9 @@ else hhash.len = hdata->ptr; } -if ( (errstr = exim_dkim_signing_init(privkey, &sctx)) - || (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig))) +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) @@ -1324,6 +1419,7 @@ if ( (errstr = exim_dkim_signing_init(privkey, &sctx)) privkey); return FALSE; } + return TRUE; } @@ -1333,7 +1429,7 @@ static gstring * arc_sign_append_sig(gstring * g, blob * sig) { /*debug_printf("%s: raw sig %.*H\n", __FUNCTION__, sig->len, sig->data);*/ -sig->data = pdkim_encode_base64(sig); +sig->data = b64encode(sig->data, sig->len); sig->len = Ustrlen(sig->data); for (;;) { @@ -1360,7 +1456,8 @@ arc_sign_append_ams(gstring * g, arc_ctx * ctx, int instance, uschar * s; gstring * hdata = NULL; int col; -int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */ +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); @@ -1378,7 +1475,7 @@ if (options & ARC_SIGN_OPT_TSTAMP) 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=", - pdkim_encode_base64(bodyhash)); + b64encode(bodyhash->data, bodyhash->len)); for(col = 3; rheaders; rheaders = rheaders->prev) { @@ -1406,7 +1503,8 @@ for(col = 3; rheaders; rheaders = rheaders->prev) /* Accumulate header for hashing/signing */ hdata = string_cat(hdata, - pdkim_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */ + /*XXX dkim module */ + arc_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */ break; } } @@ -1420,7 +1518,8 @@ g = string_catn(g, US";\r\n\tb=;", 7); /* Include the pseudo-header in the accumulation */ -s = pdkim_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE); +/*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 */ @@ -1483,7 +1582,8 @@ header_line * h = (header_line *)(al+1); uschar * badline_str; gstring * hdata = NULL; -int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */ +const blob as_h = {.data = US"sha256", .len = 6}; /*XXX hardwired */ +int hashtype = arc_dkim_hashname_blob_to_type(&as_h); blob sig; /* @@ -1533,15 +1633,18 @@ for (arc_set * as = Ustrcmp(status, US"fail") == 0 badline_str = US"aar"; if (!(l = as->hdr_aar)) goto badline; h = l->complete; - hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE)); + /*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; - hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE)); + /*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; - hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, !!as->next)); + /*XXX dkim module */ + hdata = string_cat(hdata, arc_relax_header_n(h->text, h->slen, !!as->next)); } /* Calculate the signature from the accumulation */ @@ -1567,21 +1670,19 @@ badline: /**************************************/ -/* Return pointer to pdkim_bodyhash for given hash method, creating new -method if needed. -*/ +/*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) { -int canon_head, canon_body; +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"); -pdkim_cstring_to_canons(US"relaxed", 7, &canon_head, &canon_body); /*XXX hardwired */ -return pdkim_set_bodyhash(&dkim_sign_ctx, - pdkim_hashname_to_hashtype(US"sha256", 6), /*XXX hardwired */ - canon_body, - -1); + +return arc_set_bodyhash(TRUE, &canon, &hash, -1); } @@ -1767,6 +1868,10 @@ g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar); - ? 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(); @@ -1827,8 +1932,6 @@ arc_line al; pdkim_bodyhash * b; uschar * errstr; -if (!dkim_verify_ctx) return US"no dkim context"; - 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"); diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index 13b203e80..d602886a0 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -167,6 +167,9 @@ Do not put spaces between # and the 'define'. #define SUPPORT_SRS #define SUPPORT_TRANSLATE_IP_ADDRESS +/* Required to support dynamic-module build */ +#define SUPPORT_DKIM + #define SYSLOG_LOG_PID #define SYSLOG_LONG_LINES diff --git a/src/src/daemon.c b/src/src/daemon.c index 456c586da..fc8c7fdd2 100644 --- a/src/src/daemon.c +++ b/src/src/daemon.c @@ -2562,19 +2562,6 @@ else /* no listening sockets, only queue-runs */ dns_pattern_init(); smtp_deliver_init(); /* Used for callouts */ -#ifndef DISABLE_DKIM - { -# ifdef MEASURE_TIMING - struct timeval t0; - gettimeofday(&t0, NULL); -# endif - dkim_exim_init(); -# ifdef MEASURE_TIMING - report_time_since(&t0, US"dkim_exim_init (delta)"); -# endif - } -#endif - #ifdef WITH_CONTENT_SCAN malware_init(); #endif diff --git a/src/src/dkim.c b/src/src/dkim.c deleted file mode 100644 index 68f074889..000000000 --- a/src/src/dkim.c +++ /dev/null @@ -1,914 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ - -/* Copyright (c) The Exim Maintainers 2020 - 2024 */ -/* Copyright (c) University of Cambridge, 1995 - 2018 */ -/* See the file NOTICE for conditions of use and distribution. */ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -/* Code for DKIM support. Other DKIM relevant code is in - receive.c, transport.c and transports/smtp.c */ - -#include "exim.h" - -#ifndef DISABLE_DKIM - -# include "pdkim/pdkim.h" - -# ifdef MACRO_PREDEF -# include "macro_predef.h" - -void -params_dkim(void) -{ -builtin_macro_create_var(US"_DKIM_SIGN_HEADERS", US PDKIM_DEFAULT_SIGN_HEADERS); -builtin_macro_create_var(US"_DKIM_OVERSIGN_HEADERS", US PDKIM_OVERSIGN_HEADERS); -} -# else /*!MACRO_PREDEF*/ - - - -pdkim_ctx dkim_sign_ctx; - -int dkim_verify_oldpool; -pdkim_ctx *dkim_verify_ctx = NULL; -pdkim_signature *dkim_cur_sig = NULL; -static const uschar * dkim_collect_error = NULL; - -#define DKIM_MAX_SIGNATURES 20 - - - -/* Look up the DKIM record in DNS for the given hostname. -Will use the first found if there are multiple. -The return string is tainted, having come from off-site. -*/ - -uschar * -dkim_exim_query_dns_txt(const uschar * name) -{ -dns_answer * dnsa = store_get_dns_answer(); -dns_scan dnss; -rmark reset_point = store_mark(); -gstring * g = string_get_tainted(256, GET_TAINTED); - -lookup_dnssec_authenticated = NULL; -if (dns_lookup(dnsa, name, T_TXT, NULL) != DNS_SUCCEED) - goto bad; - -/* Search for TXT record */ - -for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); - rr; - rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) - if (rr->type == T_TXT) - { /* Copy record content to the answer buffer */ - for (int rr_offset = 0; rr_offset < rr->size; ) - { - uschar len = rr->data[rr_offset++]; - - g = string_catn(g, US(rr->data + rr_offset), len); - if (g->ptr >= PDKIM_DNS_TXT_MAX_RECLEN) - goto bad; - - rr_offset += len; - } - - /* Check if this looks like a DKIM record */ - if (Ustrncmp(g->s, "v=", 2) != 0 || strncasecmp(CS g->s, "v=dkim", 6) == 0) - { - store_free_dns_answer(dnsa); - gstring_release_unused(g); - return string_from_gstring(g); - } - - gstring_reset(g); /* overwrite previous record */ - } - -bad: -store_reset(reset_point); -store_free_dns_answer(dnsa); -return NULL; /*XXX better error detail? logging? */ -} - - -void -dkim_exim_init(void) -{ -if (f.dkim_init_done) return; -f.dkim_init_done = TRUE; -pdkim_init(); -} - - - -void -dkim_exim_verify_init(BOOL dot_stuffing) -{ -dkim_exim_init(); - -/* There is a store-reset between header & body reception for the main pool -(actually, after every header line) so cannot use that as we need the data we -store per-header, during header processing, at the end of body reception -for evaluating the signature. Any allocs done for dkim verify -memory-handling must use a different pool. We use a separate one that we -can reset per message. */ - -dkim_verify_oldpool = store_pool; -store_pool = POOL_MESSAGE; - -/* Free previous context if there is one */ - -if (dkim_verify_ctx) - pdkim_free_ctx(dkim_verify_ctx); - -/* Create new context */ - -dkim_verify_ctx = pdkim_init_verify(&dkim_exim_query_dns_txt, dot_stuffing); -dkim_collect_input = dkim_verify_ctx ? DKIM_MAX_SIGNATURES : 0; -dkim_collect_error = NULL; - -/* Start feed up with any cached data, but limited to message data */ -receive_get_cache(chunking_state == CHUNKING_LAST - ? chunking_data_left : GETC_BUFFER_UNLIMITED); - -store_pool = dkim_verify_oldpool; -} - - -/* Submit a chunk of data for verification input. -Only use the data when the feed is activated. */ -void -dkim_exim_verify_feed(uschar * data, int len) -{ -int rc; - -store_pool = POOL_MESSAGE; -if ( dkim_collect_input - && (rc = pdkim_feed(dkim_verify_ctx, data, len)) != PDKIM_OK) - { - dkim_collect_error = pdkim_errstr(rc); - log_write(0, LOG_MAIN, - "DKIM: validation error: %.100s", dkim_collect_error); - dkim_collect_input = 0; - } -store_pool = dkim_verify_oldpool; -} - - -/* Log the result for the given signature */ -static void -dkim_exim_verify_log_sig(pdkim_signature * sig) -{ -gstring * logmsg; -uschar * s; - -if (!sig) return; - -/* Remember the domain for the first pass result */ - -if ( !dkim_verify_overall - && dkim_verify_status - ? Ustrcmp(dkim_verify_status, US"pass") == 0 - : sig->verify_status == PDKIM_VERIFY_PASS - ) - dkim_verify_overall = string_copy(sig->domain); - -/* Rewrite the sig result if the ACL overrode it. This is only -needed because the DMARC code (sigh) peeks at the dkim sigs. -Mark the sig for this having been done. */ - -if ( dkim_verify_status - && ( dkim_verify_status != dkim_exim_expand_query(DKIM_VERIFY_STATUS) - || dkim_verify_reason != dkim_exim_expand_query(DKIM_VERIFY_REASON) - ) ) - { /* overridden by ACL */ - sig->verify_ext_status = -1; - if (Ustrcmp(dkim_verify_status, US"fail") == 0) - sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_FAIL; - else if (Ustrcmp(dkim_verify_status, US"invalid") == 0) - sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_INVALID; - else if (Ustrcmp(dkim_verify_status, US"none") == 0) - sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_NONE; - else if (Ustrcmp(dkim_verify_status, US"pass") == 0) - sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_PASS; - else - sig->verify_status = -1; - } - -if (!LOGGING(dkim_verbose)) return; - - -logmsg = string_catn(NULL, US"DKIM: ", 6); -if (!(s = sig->domain)) s = US""; -logmsg = string_append(logmsg, 2, "d=", s); -if (!(s = sig->selector)) s = US""; -logmsg = string_append(logmsg, 2, " s=", s); -logmsg = string_fmt_append(logmsg, " c=%s/%s a=%s b=" SIZE_T_FMT, - sig->canon_headers == PDKIM_CANON_SIMPLE ? "simple" : "relaxed", - sig->canon_body == PDKIM_CANON_SIMPLE ? "simple" : "relaxed", - dkim_sig_to_a_tag(sig), - (int)sig->sighash.len > -1 ? sig->sighash.len * 8 : (size_t)0); -if ((s= sig->identity)) logmsg = string_append(logmsg, 2, " i=", s); -if (sig->created > 0) logmsg = string_fmt_append(logmsg, " t=%lu", - sig->created); -if (sig->expires > 0) logmsg = string_fmt_append(logmsg, " x=%lu", - sig->expires); -if (sig->bodylength > -1) logmsg = string_fmt_append(logmsg, " l=%lu", - sig->bodylength); - -if (sig->verify_status & PDKIM_VERIFY_POLICY) - logmsg = string_append(logmsg, 5, - US" [", dkim_verify_status, US" - ", dkim_verify_reason, US"]"); -else - switch (sig->verify_status) - { - case PDKIM_VERIFY_NONE: - logmsg = string_cat(logmsg, US" [not verified]"); - break; - - case PDKIM_VERIFY_INVALID: - logmsg = string_cat(logmsg, US" [invalid - "); - switch (sig->verify_ext_status) - { - case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: - logmsg = string_cat(logmsg, - US"public key record (currently?) unavailable]"); - break; - - case PDKIM_VERIFY_INVALID_BUFFER_SIZE: - logmsg = string_cat(logmsg, US"overlong public key record]"); - break; - - case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: - case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: - logmsg = string_cat(logmsg, US"syntax error in public key record]"); - break; - - case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: - logmsg = string_cat(logmsg, US"signature tag missing or invalid]"); - break; - - case PDKIM_VERIFY_INVALID_DKIM_VERSION: - logmsg = string_cat(logmsg, US"unsupported DKIM version]"); - break; - - default: - logmsg = string_cat(logmsg, US"unspecified problem]"); - } - break; - - case PDKIM_VERIFY_FAIL: - logmsg = string_cat(logmsg, US" [verification failed - "); - switch (sig->verify_ext_status) - { - case PDKIM_VERIFY_FAIL_BODY: - logmsg = string_cat(logmsg, - US"body hash mismatch (body probably modified in transit)]"); - break; - - case PDKIM_VERIFY_FAIL_MESSAGE: - logmsg = string_cat(logmsg, - US"signature did not verify " - "(headers probably modified in transit)]"); - break; - - case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: - logmsg = string_cat(logmsg, - US"signature invalid (key too short)]"); - break; - - default: - logmsg = string_cat(logmsg, US"unspecified reason]"); - } - break; - - case PDKIM_VERIFY_PASS: - logmsg = string_cat(logmsg, US" [verification succeeded]"); - break; - } - -log_write(0, LOG_MAIN, "%Y", logmsg); -return; -} - - -/* Log a line for each signature */ -void -dkim_exim_verify_log_all(void) -{ -for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) - dkim_exim_verify_log_sig(sig); -} - - -void -dkim_exim_verify_finish(void) -{ -int rc; -gstring * g = NULL; -const uschar * errstr = NULL; - -store_pool = POOL_MESSAGE; - -/* Delete eventual previous signature chain */ - -dkim_signers = NULL; -dkim_signatures = NULL; - -if (dkim_collect_error) - { - log_write(0, LOG_MAIN, - "DKIM: Error during validation, disabling signature verification: %.100s", - dkim_collect_error); - f.dkim_disable_verify = TRUE; - goto out; - } - -dkim_collect_input = 0; - -/* Finish DKIM operation and fetch link to signatures chain */ - -rc = pdkim_feed_finish(dkim_verify_ctx, (pdkim_signature **)&dkim_signatures, - &errstr); -if (rc != PDKIM_OK && errstr) - log_write(0, LOG_MAIN, "DKIM: validation error: %s", errstr); - -/* Build a colon-separated list of signing domains (and identities, if present) in dkim_signers */ - -for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) - { - if (sig->domain) g = string_append_listele(g, ':', sig->domain); - if (sig->identity) g = string_append_listele(g, ':', sig->identity); - } -gstring_release_unused(g); -dkim_signers = string_from_gstring(g); - -out: -store_pool = dkim_verify_oldpool; -} - - - -/* Args as per dkim_exim_acl_run() below */ -static int -dkim_acl_call(uschar * id, gstring ** res_ptr, - uschar ** user_msgptr, uschar ** log_msgptr) -{ -int rc; -DEBUG(D_receive) - debug_printf("calling acl_smtp_dkim for identity '%s' domain '%s' sel '%s'\n", - id, dkim_signing_domain, dkim_signing_selector); - -rc = acl_check(ACL_WHERE_DKIM, NULL, acl_smtp_dkim, user_msgptr, log_msgptr); -dkim_exim_verify_log_sig(dkim_cur_sig); -*res_ptr = string_append_listele(*res_ptr, ':', dkim_verify_status); -return rc; -} - - - -/* For the given identity, run the DKIM ACL once for each matching signature. -If none match, run it once. - -Arguments - id Identity to look for in dkim signatures - res_ptr ptr to growable string-list of status results, - appended to per ACL run - user_msgptr where to put a user error (for SMTP response) - log_msgptr where to put a logging message (not for SMTP response) - -Returns: OK access is granted by an ACCEPT verb - DISCARD access is granted by a DISCARD verb - FAIL access is denied - FAIL_DROP access is denied; drop the connection - DEFER can't tell at the moment - ERROR disaster -*/ - -int -dkim_exim_acl_run(uschar * id, gstring ** res_ptr, - uschar ** user_msgptr, uschar ** log_msgptr) -{ -uschar * cmp_val; -int rc = -1; - -dkim_verify_status = US"none"; -dkim_verify_reason = US""; -dkim_cur_signer = id; - -if (f.dkim_disable_verify || !id || !dkim_verify_ctx) - return OK; - -/* Find signatures to run ACL on */ - -for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) - if ( (cmp_val = Ustrchr(id, '@') != NULL ? US sig->identity : US sig->domain) - && strcmpic(cmp_val, id) == 0 - ) - { - /* The "dkim_domain" and "dkim_selector" expansion variables have - related globals, since they are used in the signing code too. - Instead of inventing separate names for verification, we set - them here. This is easy since a domain and selector is guaranteed - to be in a signature. The other dkim_* expansion items are - dynamically fetched from dkim_cur_sig at expansion time (see - dkim_exim_expand_query() below). */ - - dkim_cur_sig = sig; - dkim_signing_domain = US sig->domain; - dkim_signing_selector = US sig->selector; - dkim_key_length = sig->keybits; - - /* These two return static strings, so we can compare the addr - later to see if the ACL overwrote them. Check that when logging */ - - dkim_verify_status = dkim_exim_expand_query(DKIM_VERIFY_STATUS); - dkim_verify_reason = dkim_exim_expand_query(DKIM_VERIFY_REASON); - - if ( (rc = dkim_acl_call(id, res_ptr, user_msgptr, log_msgptr)) != OK - || dkim_verify_minimal && Ustrcmp(dkim_verify_status, "pass") == 0) - return rc; - } - -if (rc != -1) - return rc; - -/* No matching sig found. Call ACL once anyway. */ - -dkim_cur_sig = NULL; -return dkim_acl_call(id, res_ptr, user_msgptr, log_msgptr); -} - - -static uschar * -dkim_exim_expand_defaults(int what) -{ -switch (what) - { - case DKIM_ALGO: return US""; - case DKIM_BODYLENGTH: return US"9999999999999"; - case DKIM_CANON_BODY: return US""; - case DKIM_CANON_HEADERS: return US""; - case DKIM_COPIEDHEADERS: return US""; - case DKIM_CREATED: return US"0"; - case DKIM_EXPIRES: return US"9999999999999"; - case DKIM_HEADERNAMES: return US""; - case DKIM_IDENTITY: return US""; - case DKIM_KEY_GRANULARITY: return US"*"; - case DKIM_KEY_SRVTYPE: return US"*"; - case DKIM_KEY_NOTES: return US""; - case DKIM_KEY_TESTING: return US"0"; - case DKIM_NOSUBDOMAINS: return US"0"; - case DKIM_VERIFY_STATUS: return US"none"; - case DKIM_VERIFY_REASON: return US""; - default: return US""; - } -} - - -uschar * -dkim_exim_expand_query(int what) -{ -if (!dkim_verify_ctx || f.dkim_disable_verify || !dkim_cur_sig) - return dkim_exim_expand_defaults(what); - -switch (what) - { - case DKIM_ALGO: - return dkim_sig_to_a_tag(dkim_cur_sig); - - case DKIM_BODYLENGTH: - return dkim_cur_sig->bodylength >= 0 - ? string_sprintf("%ld", dkim_cur_sig->bodylength) - : dkim_exim_expand_defaults(what); - - case DKIM_CANON_BODY: - switch (dkim_cur_sig->canon_body) - { - case PDKIM_CANON_RELAXED: return US"relaxed"; - case PDKIM_CANON_SIMPLE: - default: return US"simple"; - } - - case DKIM_CANON_HEADERS: - switch (dkim_cur_sig->canon_headers) - { - case PDKIM_CANON_RELAXED: return US"relaxed"; - case PDKIM_CANON_SIMPLE: - default: return US"simple"; - } - - case DKIM_COPIEDHEADERS: - return dkim_cur_sig->copiedheaders - ? US dkim_cur_sig->copiedheaders : dkim_exim_expand_defaults(what); - - case DKIM_CREATED: - return dkim_cur_sig->created > 0 - ? string_sprintf("%lu", dkim_cur_sig->created) - : dkim_exim_expand_defaults(what); - - case DKIM_EXPIRES: - return dkim_cur_sig->expires > 0 - ? string_sprintf("%lu", dkim_cur_sig->expires) - : dkim_exim_expand_defaults(what); - - case DKIM_HEADERNAMES: - return dkim_cur_sig->headernames - ? dkim_cur_sig->headernames : dkim_exim_expand_defaults(what); - - case DKIM_IDENTITY: - return dkim_cur_sig->identity - ? US dkim_cur_sig->identity : dkim_exim_expand_defaults(what); - - case DKIM_KEY_GRANULARITY: - return dkim_cur_sig->pubkey - ? dkim_cur_sig->pubkey->granularity - ? US dkim_cur_sig->pubkey->granularity - : dkim_exim_expand_defaults(what) - : dkim_exim_expand_defaults(what); - - case DKIM_KEY_SRVTYPE: - return dkim_cur_sig->pubkey - ? dkim_cur_sig->pubkey->srvtype - ? US dkim_cur_sig->pubkey->srvtype - : dkim_exim_expand_defaults(what) - : dkim_exim_expand_defaults(what); - - case DKIM_KEY_NOTES: - return dkim_cur_sig->pubkey - ? dkim_cur_sig->pubkey->notes - ? US dkim_cur_sig->pubkey->notes - : dkim_exim_expand_defaults(what) - : dkim_exim_expand_defaults(what); - - case DKIM_KEY_TESTING: - return dkim_cur_sig->pubkey - ? dkim_cur_sig->pubkey->testing - ? US"1" - : dkim_exim_expand_defaults(what) - : dkim_exim_expand_defaults(what); - - case DKIM_NOSUBDOMAINS: - return dkim_cur_sig->pubkey - ? dkim_cur_sig->pubkey->no_subdomaining - ? US"1" - : dkim_exim_expand_defaults(what) - : dkim_exim_expand_defaults(what); - - case DKIM_VERIFY_STATUS: - switch (dkim_cur_sig->verify_status) - { - case PDKIM_VERIFY_INVALID: return US"invalid"; - case PDKIM_VERIFY_FAIL: return US"fail"; - case PDKIM_VERIFY_PASS: return US"pass"; - case PDKIM_VERIFY_NONE: - default: return US"none"; - } - - case DKIM_VERIFY_REASON: - switch (dkim_cur_sig->verify_ext_status) - { - case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: - return US"pubkey_unavailable"; - case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD:return US"pubkey_dns_syntax"; - case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return US"pubkey_der_syntax"; - case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: return US"pubkey_too_short"; - case PDKIM_VERIFY_FAIL_BODY: return US"bodyhash_mismatch"; - case PDKIM_VERIFY_FAIL_MESSAGE: return US"signature_incorrect"; - } - - default: - return US""; - } -} - - -void -dkim_exim_sign_init(void) -{ -int old_pool = store_pool; - -dkim_exim_init(); -store_pool = POOL_MAIN; -pdkim_init_context(&dkim_sign_ctx, FALSE, &dkim_exim_query_dns_txt); -store_pool = old_pool; -} - - -/* Generate signatures for the given file. -If a prefix is given, prepend it to the file for the calculations. - -Return: - NULL: error; error string written - string: signature header(s), or a zero-length string (not an error) -*/ - -gstring * -dkim_exim_sign(int fd, off_t off, uschar * prefix, - struct ob_dkim * dkim, const uschar ** errstr) -{ -const uschar * dkim_domain = NULL; -int sep = 0; -gstring * seen_doms = NULL; -pdkim_signature * sig; -gstring * sigbuf; -int pdkim_rc; -int sread; -uschar buf[4096]; -int save_errno = 0; -int old_pool = store_pool; -uschar * errwhen; -const uschar * s; - -if (dkim->dot_stuffed) - dkim_sign_ctx.flags |= PDKIM_DOT_TERM; - -store_pool = POOL_MAIN; - -GET_OPTION("dkim_domain"); -if ((s = dkim->dkim_domain) && !(dkim_domain = expand_cstring(s))) - /* expansion error, do not send message. */ - { errwhen = US"dkim_domain"; goto expand_bad; } - -/* Set $dkim_domain expansion variable to each unique domain in list. */ - -if (dkim_domain) - while ((dkim_signing_domain = string_nextinlist(&dkim_domain, &sep, NULL, 0))) - { - const uschar * dkim_sel; - int sel_sep = 0; - - if (dkim_signing_domain[0] == '\0') - continue; - - /* Only sign once for each domain, no matter how often it - appears in the expanded list. */ - - dkim_signing_domain = string_copylc(dkim_signing_domain); - if (match_isinlist(dkim_signing_domain, CUSS &seen_doms, - 0, NULL, NULL, MCL_STRING, TRUE, NULL) == OK) - continue; - - seen_doms = string_append_listele(seen_doms, ':', dkim_signing_domain); - - /* Set $dkim_selector expansion variable to each selector in list, - for this domain. */ - - GET_OPTION("dkim_selector"); - if (!(dkim_sel = expand_string(dkim->dkim_selector))) - { errwhen = US"dkim_selector"; goto expand_bad; } - - while ((dkim_signing_selector = string_nextinlist(&dkim_sel, &sel_sep, - NULL, 0))) - { - uschar * dkim_canon_expanded; - int pdkim_canon; - uschar * dkim_sign_headers_expanded = NULL; - uschar * dkim_private_key_expanded; - uschar * dkim_hash_expanded; - uschar * dkim_identity_expanded = NULL; - uschar * dkim_timestamps_expanded = NULL; - unsigned long tval = 0, xval = 0; - - /* Get canonicalization to use */ - - GET_OPTION("dkim_canon"); - dkim_canon_expanded = dkim->dkim_canon - ? expand_string(dkim->dkim_canon) : US"relaxed"; - if (!dkim_canon_expanded) /* expansion error, do not send message. */ - { errwhen = US"dkim_canon"; goto expand_bad; } - - if (Ustrcmp(dkim_canon_expanded, "relaxed") == 0) - pdkim_canon = PDKIM_CANON_RELAXED; - else if (Ustrcmp(dkim_canon_expanded, "simple") == 0) - pdkim_canon = PDKIM_CANON_SIMPLE; - else - { - log_write(0, LOG_MAIN, - "DKIM: unknown canonicalization method '%s', defaulting to 'relaxed'.\n", - dkim_canon_expanded); - pdkim_canon = PDKIM_CANON_RELAXED; - } - - GET_OPTION("dkim_sign_headers"); - if ( dkim->dkim_sign_headers - && !(dkim_sign_headers_expanded = expand_string(dkim->dkim_sign_headers))) - { errwhen = US"dkim_sign_header"; goto expand_bad; } - /* else pass NULL, which means default header list */ - - /* Get private key to use. */ - - GET_OPTION("dkim_private_key"); - if (!(dkim_private_key_expanded = expand_string(dkim->dkim_private_key))) - { errwhen = US"dkim_private_key"; goto expand_bad; } - - if ( Ustrlen(dkim_private_key_expanded) == 0 - || Ustrcmp(dkim_private_key_expanded, "0") == 0 - || Ustrcmp(dkim_private_key_expanded, "false") == 0 - ) - continue; /* don't sign, but no error */ - - if ( dkim_private_key_expanded[0] == '/' - && !(dkim_private_key_expanded = - expand_file_big_buffer(dkim_private_key_expanded))) - goto bad; - - GET_OPTION("dkim_hash"); - if (!(dkim_hash_expanded = expand_string(dkim->dkim_hash))) - { errwhen = US"dkim_hash"; goto expand_bad; } - - GET_OPTION("dkim_identity"); - if (dkim->dkim_identity) - if (!(dkim_identity_expanded = expand_string(dkim->dkim_identity))) - { errwhen = US"dkim_identity"; goto expand_bad; } - else if (!*dkim_identity_expanded) - dkim_identity_expanded = NULL; - - GET_OPTION("dkim_timestamps"); - if (dkim->dkim_timestamps) - if (!(dkim_timestamps_expanded = expand_string(dkim->dkim_timestamps))) - { errwhen = US"dkim_timestamps"; goto expand_bad; } - else - { - tval = (unsigned long) time(NULL); - xval = strtoul(CCS dkim_timestamps_expanded, NULL, 10); - if (xval > 0) - xval += tval; - } - - if (!(sig = pdkim_init_sign(&dkim_sign_ctx, dkim_signing_domain, - dkim_signing_selector, - dkim_private_key_expanded, - dkim_hash_expanded, - errstr - ))) - goto bad; - dkim_private_key_expanded[0] = '\0'; - - pdkim_set_optional(sig, - CS dkim_sign_headers_expanded, - CS dkim_identity_expanded, - pdkim_canon, - pdkim_canon, -1, tval, xval); - - if (!pdkim_set_sig_bodyhash(&dkim_sign_ctx, sig)) - goto bad; - - dkim_signing_record = string_append_listele(dkim_signing_record, ':', dkim_signing_domain); - dkim_signing_record = string_append_listele(dkim_signing_record, ':', dkim_signing_selector); - - if (!dkim_sign_ctx.sig) /* link sig to context chain */ - dkim_sign_ctx.sig = sig; - else - { - pdkim_signature * n = dkim_sign_ctx.sig; - while (n->next) n = n->next; - n->next = sig; - } - } - } - -/* We may need to carry on with the data-feed even if there are no DKIM sigs to -produce, if some other package (eg. ARC) is signing. */ - -if (!dkim_sign_ctx.sig && !dkim->force_bodyhash) - { - DEBUG(D_transport) debug_printf("DKIM: no viable signatures to use\n"); - sigbuf = string_get(1); /* return a zero-len string */ - } -else - { - if (prefix && (pdkim_rc = pdkim_feed(&dkim_sign_ctx, prefix, Ustrlen(prefix))) != PDKIM_OK) - goto pk_bad; - - if (lseek(fd, off, SEEK_SET) < 0) - sread = -1; - else - while ((sread = read(fd, &buf, sizeof(buf))) > 0) - if ((pdkim_rc = pdkim_feed(&dkim_sign_ctx, buf, sread)) != PDKIM_OK) - goto pk_bad; - - /* Handle failed read above. */ - if (sread == -1) - { - debug_printf("DKIM: Error reading -K file.\n"); - save_errno = errno; - goto bad; - } - - /* Build string of headers, one per signature */ - - if ((pdkim_rc = pdkim_feed_finish(&dkim_sign_ctx, &sig, errstr)) != PDKIM_OK) - goto pk_bad; - - if (!sig) - { - DEBUG(D_transport) debug_printf("DKIM: no signatures to use\n"); - sigbuf = string_get(1); /* return a zero-len string */ - } - else for (sigbuf = NULL; sig; sig = sig->next) - sigbuf = string_append(sigbuf, 2, US sig->signature_header, US"\r\n"); - } - -CLEANUP: - (void) string_from_gstring(sigbuf); - store_pool = old_pool; - errno = save_errno; - return sigbuf; - -pk_bad: - log_write(0, LOG_MAIN|LOG_PANIC, - "DKIM: signing failed: %.100s", pdkim_errstr(pdkim_rc)); -bad: - sigbuf = NULL; - goto CLEANUP; - -expand_bad: - *errstr = string_sprintf("failed to expand %s: %s", - errwhen, expand_string_message); - log_write(0, LOG_MAIN | LOG_PANIC, "%s", *errstr); - goto bad; -} - - - - -gstring * -authres_dkim(gstring * g) -{ -int start = 0; /* compiler quietening */ - -DEBUG(D_acl) start = gstring_length(g); - -for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) - { - g = string_catn(g, US";\n\tdkim=", 8); - - if (sig->verify_status & PDKIM_VERIFY_POLICY) - g = string_append(g, 5, - US"policy (", dkim_verify_status, US" - ", dkim_verify_reason, US")"); - else switch(sig->verify_status) - { - case PDKIM_VERIFY_NONE: g = string_cat(g, US"none"); break; - case PDKIM_VERIFY_INVALID: - switch (sig->verify_ext_status) - { - case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: - g = string_cat(g, US"tmperror (pubkey unavailable)\n\t\t"); break; - case PDKIM_VERIFY_INVALID_BUFFER_SIZE: - g = string_cat(g, US"permerror (overlong public key record)\n\t\t"); break; - case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: - case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: - g = string_cat(g, US"neutral (public key record import problem)\n\t\t"); - break; - case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: - g = string_cat(g, US"neutral (signature tag missing or invalid)\n\t\t"); - break; - case PDKIM_VERIFY_INVALID_DKIM_VERSION: - g = string_cat(g, US"neutral (unsupported DKIM version)\n\t\t"); - break; - default: - g = string_cat(g, US"permerror (unspecified problem)\n\t\t"); break; - } - break; - case PDKIM_VERIFY_FAIL: - switch (sig->verify_ext_status) - { - case PDKIM_VERIFY_FAIL_BODY: - g = string_cat(g, - US"fail (body hash mismatch; body probably modified in transit)\n\t\t"); - break; - case PDKIM_VERIFY_FAIL_MESSAGE: - g = string_cat(g, - US"fail (signature did not verify; headers probably modified in transit)\n\t\t"); - break; - case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: /* should this really be "polcy"? */ - g = string_fmt_append(g, "fail (public key too short: %u bits)\n\t\t", sig->keybits); - break; - default: - g = string_cat(g, US"fail (unspecified reason)\n\t\t"); - break; - } - break; - case PDKIM_VERIFY_PASS: g = string_cat(g, US"pass"); break; - default: g = string_cat(g, US"permerror"); break; - } - if (sig->domain) g = string_append(g, 2, US" header.d=", sig->domain); - if (sig->identity) g = string_append(g, 2, US" header.i=", sig->identity); - if (sig->selector) g = string_append(g, 2, US" header.s=", sig->selector); - g = string_append(g, 2, US" header.a=", dkim_sig_to_a_tag(sig)); - } - -DEBUG(D_acl) - if (gstring_length(g) == start) - debug_printf("DKIM:\tno authres\n"); - else - debug_printf("DKIM:\tauthres '%.*s'\n", g->ptr - start - 3, g->s + start + 3); -return g; -} - - -# endif /*!MACRO_PREDEF*/ -#endif /*!DISABLE_DKIM*/ diff --git a/src/src/dkim.h b/src/src/dkim.h deleted file mode 100644 index 915c6c739..000000000 --- a/src/src/dkim.h +++ /dev/null @@ -1,33 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ - -/* Copyright (c) University of Cambridge, 1995 - 2018 */ -/* See the file NOTICE for conditions of use and distribution. */ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -void dkim_exim_init(void); -gstring * dkim_exim_sign(int, off_t, uschar *, struct ob_dkim *, const uschar **); -void dkim_exim_verify_init(BOOL); -void dkim_exim_verify_feed(uschar *, int); -void dkim_exim_verify_finish(void); -void dkim_exim_verify_log_all(void); -int dkim_exim_acl_run(uschar *, gstring **, uschar **, uschar **); -uschar *dkim_exim_expand_query(int); - -#define DKIM_ALGO 1 -#define DKIM_BODYLENGTH 2 -#define DKIM_CANON_BODY 3 -#define DKIM_CANON_HEADERS 4 -#define DKIM_COPIEDHEADERS 5 -#define DKIM_CREATED 6 -#define DKIM_EXPIRES 7 -#define DKIM_HEADERNAMES 8 -#define DKIM_IDENTITY 9 -#define DKIM_KEY_GRANULARITY 10 -#define DKIM_KEY_SRVTYPE 11 -#define DKIM_KEY_NOTES 12 -#define DKIM_KEY_TESTING 13 -#define DKIM_NOSUBDOMAINS 14 -#define DKIM_VERIFY_STATUS 15 -#define DKIM_VERIFY_REASON 16 diff --git a/src/src/dkim_transport.c b/src/src/dkim_transport.c deleted file mode 100644 index 63870c57f..000000000 --- a/src/src/dkim_transport.c +++ /dev/null @@ -1,420 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ - -/* Copyright (c) The Exim Maintainers 2022 - 2024 */ -/* Copyright (c) University of Cambridge 1995 - 2018 */ -/* See the file NOTICE for conditions of use and distribution. */ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -/* Transport shim for dkim signing */ - - -#include "exim.h" - -#ifndef DISABLE_DKIM /* rest of file */ - - -static BOOL -dkt_sign_fail(struct ob_dkim * dkim, int * errp) -{ -GET_OPTION("dkim_strict"); -if (dkim->dkim_strict) - { - uschar * dkim_strict_result = expand_string(dkim->dkim_strict); - - if (dkim_strict_result) - if ( strcmpic(dkim_strict_result, US"1") == 0 - || strcmpic(dkim_strict_result, US"true") == 0) - { - /* Set errno to something halfway meaningful */ - *errp = EACCES; - log_write(0, LOG_MAIN, "DKIM: message could not be signed," - " and dkim_strict is set. Deferring message delivery."); - return FALSE; - } - } -return TRUE; -} - -/* Send the file at in_fd down the output fd */ - -static BOOL -dkt_send_file(int out_fd, int in_fd, off_t off -#ifdef OS_SENDFILE - , size_t size -#endif - ) -{ -#ifdef OS_SENDFILE -DEBUG(D_transport) debug_printf("send file fd=%d size=%u\n", out_fd, (unsigned)(size - off)); -#else -DEBUG(D_transport) debug_printf("send file fd=%d\n", out_fd); -#endif - -/*XXX should implement timeout, like transport_write_block_fd() ? */ - -#ifdef OS_SENDFILE -/* We can use sendfile() to shove the file contents - to the socket. However only if we don't use TLS, - as then there's another layer of indirection - before the data finally hits the socket. */ -if (tls_out.active.sock != out_fd) - { - ssize_t copied = 0; - - while(copied >= 0 && off < size) - copied = os_sendfile(out_fd, in_fd, &off, size - off); - if (copied < 0) - return FALSE; - } -else - -#endif - - { - int sread, wwritten; - - /* Rewind file */ - if (lseek(in_fd, off, SEEK_SET) < 0) return FALSE; - - /* Send file down the original fd */ - while((sread = read(in_fd, deliver_out_buffer, DELIVER_OUT_BUFFER_SIZE)) > 0) - { - uschar * p = deliver_out_buffer; - /* write the chunk */ - - while (sread) - { -#ifndef DISABLE_TLS - wwritten = tls_out.active.sock == out_fd - ? tls_write(tls_out.active.tls_ctx, p, sread, FALSE) - : write(out_fd, CS p, sread); -#else - wwritten = write(out_fd, CS p, sread); -#endif - if (wwritten == -1) - return FALSE; - p += wwritten; - sread -= wwritten; - } - } - - if (sread == -1) - return FALSE; - } - -return TRUE; -} - - - - -/* This function is a wrapper around transport_write_message(). - It is only called from the smtp transport if DKIM or Domainkeys support - is active and no transport filter is to be used. - -Arguments: - As for transport_write_message() in transort.c, with additional arguments - for DKIM. - -Returns: TRUE on success; FALSE (with errno) for any failure -*/ - -static BOOL -dkt_direct(transport_ctx * tctx, struct ob_dkim * dkim, - const uschar ** err) -{ -int save_fd = tctx->u.fd; -int save_options = tctx->options; -BOOL save_wireformat = f.spool_file_wireformat; -uschar * hdrs; -gstring * dkim_signature; -int hsize; -const uschar * errstr; -BOOL rc; - -DEBUG(D_transport) debug_printf("dkim signing direct-mode\n"); - -/* Get headers in string for signing and transmission. Do CRLF -and dotstuffing (but no body nor dot-termination) */ - -tctx->u.msg = NULL; -tctx->options = tctx->options & ~(topt_end_dot | topt_use_bdat) - | topt_output_string | topt_no_body; - -rc = transport_write_message(tctx, 0); -hdrs = string_from_gstring(tctx->u.msg); -hsize = tctx->u.msg->ptr; - -tctx->u.fd = save_fd; -tctx->options = save_options; -if (!rc) return FALSE; - -/* Get signatures for headers plus spool data file */ - -#ifdef EXPERIMENTAL_ARC -arc_sign_init(); -#endif - -/* The dotstuffed status of the datafile depends on whether it was stored -in wireformat. */ - -dkim->dot_stuffed = f.spool_file_wireformat; -if (!(dkim_signature = dkim_exim_sign(deliver_datafile, - spool_data_start_offset(message_id), hdrs, dkim, &errstr))) - if (!(rc = dkt_sign_fail(dkim, &errno))) - { - *err = errstr; - return FALSE; - } - -#ifdef EXPERIMENTAL_ARC -if (dkim->arc_signspec) /* Prepend ARC headers */ - { - uschar * e = NULL; - if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, &e))) - { - *err = e; - return FALSE; - } - } -#endif - -/* Write the signature and headers into the deliver-out-buffer. This should -mean they go out in the same packet as the MAIL, RCPT and (first) BDAT commands -(transport_write_message() sizes the BDAT for the buffered amount) - for short -messages, the BDAT LAST command. We want no dotstuffing expansion here, it -having already been done - but we have to say we want CRLF output format, and -temporarily set the marker for possible already-CRLF input. */ - -tctx->options &= ~topt_escape_headers; -f.spool_file_wireformat = TRUE; -transport_write_reset(0); -if ( ( dkim_signature - && dkim_signature->ptr > 0 - && !write_chunk(tctx, dkim_signature->s, dkim_signature->ptr) - ) - || !write_chunk(tctx, hdrs, hsize) - ) - return FALSE; - -f.spool_file_wireformat = save_wireformat; -tctx->options = save_options | topt_no_headers | topt_continuation; - -if (!(transport_write_message(tctx, 0))) - return FALSE; - -tctx->options = save_options; -return TRUE; -} - - -/* This function is a wrapper around transport_write_message(). - It is only called from the smtp transport if DKIM or Domainkeys support - is active and a transport filter is to be used. The function sets up a - replacement fd into a -K file, then calls the normal function. This way, the - exact bits that exim would have put "on the wire" will end up in the file - (except for TLS encapsulation, which is the very very last thing). When we - are done signing the file, send the signed message down the original fd (or - TLS fd). - -Arguments: - As for transport_write_message() in transort.c, with additional arguments - for DKIM. - -Returns: TRUE on success; FALSE (with errno) for any failure -*/ - -static BOOL -dkt_via_kfile(transport_ctx * tctx, struct ob_dkim * dkim, const uschar ** err) -{ -int dkim_fd; -int save_errno = 0; -BOOL rc; -uschar * dkim_spool_name; -gstring * dkim_signature; -int options, dlen; -off_t k_file_size; -const uschar * errstr; - -dkim_spool_name = spool_fname(US"input", message_subdir, message_id, - string_sprintf("-%d-K", (int)getpid())); - -DEBUG(D_transport) debug_printf("dkim signing via file %s\n", dkim_spool_name); - -if ((dkim_fd = Uopen(dkim_spool_name, O_RDWR|O_CREAT|O_TRUNC, SPOOL_MODE)) < 0) - { - /* Can't create spool file. Ugh. */ - rc = FALSE; - save_errno = errno; - *err = string_sprintf("dkim spoolfile create: %s", strerror(errno)); - goto CLEANUP; - } - -/* Call transport utility function to write the -K file; does the CRLF expansion -(but, in the CHUNKING case, neither dot-stuffing nor dot-termination). */ - - { - int save_fd = tctx->u.fd; - tctx->u.fd = dkim_fd; - options = tctx->options; - tctx->options &= ~topt_use_bdat; - - rc = transport_write_message(tctx, 0); - - tctx->u.fd = save_fd; - tctx->options = options; - } - -/* Save error state. We must clean up before returning. */ -if (!rc) - { - save_errno = errno; - 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. */ - -dkim->dot_stuffed = !!(options & topt_end_dot); -if (!(dkim_signature = dkim_exim_sign(dkim_fd, 0, NULL, dkim, &errstr))) - { - dlen = 0; - if (!(rc = dkt_sign_fail(dkim, &save_errno))) - { - *err = errstr; - goto CLEANUP; - } - } -else - dlen = dkim_signature->ptr; - -#ifdef EXPERIMENTAL_ARC -if (dkim->arc_signspec) /* Prepend ARC headers */ - { - if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, USS err))) - goto CLEANUP; - dlen = dkim_signature->ptr; - } -#endif - -#ifndef OS_SENDFILE -if (options & topt_use_bdat) -#endif - if ((k_file_size = lseek(dkim_fd, 0, SEEK_END)) < 0) - { - *err = string_sprintf("dkim spoolfile seek: %s", strerror(errno)); - goto CLEANUP; - } - -if (options & topt_use_bdat) - { - /* On big messages output a precursor chunk to get any pipelined - MAIL & RCPT commands flushed, then reap the responses so we can - error out on RCPT rejects before sending megabytes. */ - - if ( dlen + k_file_size > DELIVER_OUT_BUFFER_SIZE - && dlen > 0) - { - if ( tctx->chunk_cb(tctx, dlen, 0) != OK - || !transport_write_block(tctx, - dkim_signature->s, dlen, FALSE) - || tctx->chunk_cb(tctx, 0, tc_reap_prev) != OK - ) - goto err; - dlen = 0; - } - - /* Send the BDAT command for the entire message, as a single LAST-marked - chunk. */ - - if (tctx->chunk_cb(tctx, dlen + k_file_size, tc_chunk_last) != OK) - goto err; - } - -if(dlen > 0 && !transport_write_block(tctx, dkim_signature->s, dlen, TRUE)) - goto err; - -if (!dkt_send_file(tctx->u.fd, dkim_fd, 0 -#ifdef OS_SENDFILE - , k_file_size -#endif - )) - { - save_errno = errno; - rc = FALSE; - } - -CLEANUP: - /* unlink -K file */ - if (dkim_fd >= 0) (void)close(dkim_fd); - Uunlink(dkim_spool_name); - errno = save_errno; - return rc; - -err: - save_errno = errno; - rc = FALSE; - goto CLEANUP; -} - - - -/*************************************************************************************************** -* External interface to write the message, while signing it with DKIM and/or Domainkeys * -***************************************************************************************************/ - -/* This function is a wrapper around transport_write_message(). - It is only called from the smtp transport if DKIM or Domainkeys support - is compiled in. - -Arguments: - As for transport_write_message() in transort.c, with additional arguments - for DKIM. - -Returns: TRUE on success; FALSE (with errno) for any failure -*/ - -BOOL -dkim_transport_write_message(transport_ctx * tctx, - struct ob_dkim * dkim, const uschar ** err) -{ -BOOL yield; - -/* If we can't sign, just call the original function. */ - -if ( !(dkim->dkim_private_key && dkim->dkim_domain && dkim->dkim_selector) - && !dkim->force_bodyhash) - return transport_write_message(tctx, 0); - -/* If there is no filter command set up, construct the message and calculate -a dkim signature of it, send the signature and a reconstructed message. This -avoids using a temprary file. */ - -if ( !transport_filter_argv - || !*transport_filter_argv - || !**transport_filter_argv - ) - yield = dkt_direct(tctx, dkim, err); - -else - /* Use the transport path to write a file, calculate a dkim signature, - send the signature and then send the file. */ - - yield = dkt_via_kfile(tctx, dkim, err); - -tctx->addr->dkim_used = string_from_gstring(dkim_signing_record); -return yield; -} - -#endif /* whole file */ - -/* vi: aw ai sw=2 -*/ -/* End of dkim_transport.c */ diff --git a/src/src/drtables.c b/src/src/drtables.c index 9ed55e29a..61ced3e6a 100644 --- a/src/src/drtables.c +++ b/src/src/drtables.c @@ -431,12 +431,16 @@ misc_module_info * misc_module_list = NULL; static void misc_mod_add(misc_module_info * mi) { -if (mi->init) mi->init(mi); -DEBUG(D_any) if (mi->lib_vers_report) - debug_printf_indent("%Y", mi->lib_vers_report(NULL)); +if (mi->init && mi->init(mi)) + { + DEBUG(D_any) if (mi->lib_vers_report) + debug_printf_indent("%Y", mi->lib_vers_report(NULL)); -mi->next = misc_module_list; -misc_module_list = mi; + mi->next = misc_module_list; + misc_module_list = mi; + } +else DEBUG(D_any) + debug_printf_indent("module init call failed for %s\n", mi->name); } @@ -453,7 +457,10 @@ const char * errormsg; DEBUG(D_any) debug_printf_indent("loading module '%s'\n", name); if (!(dl = mod_open(name, US"miscmod", errstr))) + { + DEBUG(D_any) debug_printf_indent(" mod_open: %s\n", *errstr); return NULL; + } mi = (struct misc_module_info *) dlsym(dl, CS string_sprintf("%s_module_info", name)); @@ -546,6 +553,39 @@ for (const misc_module_info * mi = misc_module_list; mi; mi = mi->next) return OK; } +/* Ditto, authres. Having to sort the responses (mainly for the testsuite) +is pretty painful - maybe we should sort the modules on insertion to +the list? */ + +gstring * +misc_mod_authres(gstring * g) +{ +typedef struct { + const uschar * name; + gstring * res; +} pref; +pref prefs[] = { + {US"spf", NULL}, {US"dkim", NULL}, {US"dmarc", NULL}, {US"arc", NULL} +}; +gstring * others = NULL; + +for (const misc_module_info * mi = misc_module_list; mi; mi = mi->next) + if (mi->authres) + { + pref * p; + for (p = prefs; p < prefs + nelem(prefs); p++) + if (Ustrcmp(p->name, mi->name) == 0) break; + + if (p) p->res = (mi->authres)(NULL); + else others = (mi->authres)(others); + } + +for (pref * p = prefs; p < prefs + nelem(prefs); p++) + g = gstring_append(g, p->res); +return gstring_append(g, others); +} + + @@ -697,6 +737,9 @@ DEBUG(D_lookup) debug_printf("Loaded %d lookup modules\n", countmodules); } +#if !defined(DISABLE_DKIM) && (!defined(SUPPORT_DKIM) || SUPPORT_DKIM!=2) +extern misc_module_info dkim_module_info; +#endif #if defined(SUPPORT_DMARC) && SUPPORT_DMARC!=2 extern misc_module_info dmarc_module_info; #endif @@ -709,16 +752,18 @@ init_misc_mod_list(void) { static BOOL onetime = FALSE; if (onetime) return; +onetime = TRUE; +#if !defined(DISABLE_DKIM) && (!defined(SUPPORT_DKIM) || SUPPORT_DKIM!=2) +misc_mod_add(&dkim_module_info); +#endif #if defined(SUPPORT_SPF) && SUPPORT_SPF!=2 -/* dmarc depends on spf so this add must go first, for the dmarc-static case */ 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); #endif - -onetime = TRUE; } diff --git a/src/src/exim.c b/src/src/exim.c index 5ad54ffc1..ca98e25de 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -4211,6 +4211,9 @@ 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 @@ -5610,9 +5613,6 @@ if (host_checking) return_path = sender_address = NULL; dnslist_domain = dnslist_matched = NULL; -#ifndef DISABLE_DKIM - dkim_cur_signer = NULL; -#endif acl_var_m = NULL; deliver_localpart_orig = NULL; deliver_domain_orig = NULL; diff --git a/src/src/exim.h b/src/src/exim.h index c996a2f8c..8260dc75f 100644 --- a/src/src/exim.h +++ b/src/src/exim.h @@ -547,7 +547,8 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly. # include "miscmods/spf_api.h" #endif #ifndef DISABLE_DKIM -# include "dkim.h" +# include "miscmods/dkim.h" +# include "miscmods/dkim_api.h" #endif #ifdef SUPPORT_DMARC # include "miscmods/dmarc.h" diff --git a/src/src/expand.c b/src/src/expand.c index af3816051..02680771f 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -490,27 +490,28 @@ static var_entry var_table[] = { { "dcc_result", vtype_stringptr, &dcc_result }, #endif #ifndef DISABLE_DKIM - { "dkim_algo", vtype_dkim, (void *)DKIM_ALGO }, - { "dkim_bodylength", vtype_dkim, (void *)DKIM_BODYLENGTH }, - { "dkim_canon_body", vtype_dkim, (void *)DKIM_CANON_BODY }, - { "dkim_canon_headers", vtype_dkim, (void *)DKIM_CANON_HEADERS }, - { "dkim_copiedheaders", vtype_dkim, (void *)DKIM_COPIEDHEADERS }, - { "dkim_created", vtype_dkim, (void *)DKIM_CREATED }, - { "dkim_cur_signer", vtype_stringptr, &dkim_cur_signer }, - { "dkim_domain", vtype_stringptr, &dkim_signing_domain }, - { "dkim_expires", vtype_dkim, (void *)DKIM_EXPIRES }, - { "dkim_headernames", vtype_dkim, (void *)DKIM_HEADERNAMES }, - { "dkim_identity", vtype_dkim, (void *)DKIM_IDENTITY }, - { "dkim_key_granularity",vtype_dkim, (void *)DKIM_KEY_GRANULARITY }, - { "dkim_key_length", vtype_int, &dkim_key_length }, - { "dkim_key_nosubdomains",vtype_dkim, (void *)DKIM_NOSUBDOMAINS }, - { "dkim_key_notes", vtype_dkim, (void *)DKIM_KEY_NOTES }, - { "dkim_key_srvtype", vtype_dkim, (void *)DKIM_KEY_SRVTYPE }, - { "dkim_key_testing", vtype_dkim, (void *)DKIM_KEY_TESTING }, - { "dkim_selector", vtype_stringptr, &dkim_signing_selector }, - { "dkim_signers", vtype_stringptr, &dkim_signers }, - { "dkim_verify_reason", vtype_stringptr, &dkim_verify_reason }, - { "dkim_verify_status", vtype_stringptr, &dkim_verify_status }, + { "dkim_algo", vtype_module, US"dkim" }, + { "dkim_bodylength", vtype_module, US"dkim" }, + { "dkim_canon_body", vtype_module, US"dkim" }, + { "dkim_canon_headers", vtype_module, US"dkim" }, + { "dkim_copiedheaders", vtype_module, US"dkim" }, + { "dkim_created", vtype_module, US"dkim" }, + { "dkim_cur_signer", vtype_module, US"dkim" }, + { "dkim_domain", vtype_module, US"dkim" }, + { "dkim_expires", vtype_module, US"dkim" }, + { "dkim_headernames", vtype_module, US"dkim" }, + { "dkim_identity", vtype_module, US"dkim" }, + { "dkim_key_granularity",vtype_module, US"dkim" }, + { "dkim_key_length", vtype_module, US"dkim" }, + { "dkim_key_nosubdomains",vtype_module, US"dkim" }, + { "dkim_key_notes", vtype_module, US"dkim" }, + { "dkim_key_srvtype", vtype_module, US"dkim" }, + { "dkim_key_testing", vtype_module, US"dkim" }, + { "dkim_selector", vtype_module, US"dkim" }, + { "dkim_signers", vtype_module, US"dkim" }, + { "dkim_verify_reason", vtype_module, US"dkim" }, + { "dkim_verify_signers", vtype_module, US"dkim" }, + { "dkim_verify_status", vtype_module, US"dkim" }, #endif #ifdef SUPPORT_DMARC { "dmarc_domain_policy", vtype_module, US"dmarc" }, @@ -2131,7 +2132,13 @@ switch (vp->type) #ifndef DISABLE_DKIM case vtype_dkim: - return dkim_exim_expand_query((int)(long)val); + { + misc_module_info * mi = misc_mod_findonly(US"dkim"); + typedef uschar * (*fn_t)(int); + return mi + ? (((fn_t *) mi->functions)[DKIM_EXPAND_QUERY]) ((int)(long)val) + : US""; + } #endif case vtype_module: @@ -4882,32 +4889,7 @@ while (*s) yield = authres_local(yield, sub_arg[0]); yield = authres_iprev(yield); yield = authres_smtpauth(yield); -#ifdef SUPPORT_SPF - { - misc_module_info * mi = misc_mod_findonly(US"spf"); - if (mi) - { - typedef gstring * (*fn_t)(gstring *); - fn_t fn = ((fn_t *) mi->functions)[SPF_AUTHRES]; - yield = fn(yield); - } - } -#endif -#ifndef DISABLE_DKIM - yield = authres_dkim(yield); -#endif -#ifdef SUPPORT_DMARC - { - misc_module_info * mi = misc_mod_findonly(US"dmarc"); - if (mi) - { - /*XXX is authres common enough to be generic? */ - typedef gstring * (*fn_t)(gstring *); - fn_t fn = ((fn_t *) mi->functions)[DMARC_AUTHRES]; - yield = fn(yield); - } - } -#endif + yield = misc_mod_authres(yield); #ifdef EXPERIMENTAL_ARC yield = authres_arc(yield); #endif diff --git a/src/src/functions.h b/src/src/functions.h index 3a980318f..fba5ec688 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -1,3 +1,4 @@ +extern BOOL arc_init(void); /************************************************* * Exim - an Internet mail transport agent * *************************************************/ @@ -144,9 +145,6 @@ extern uschar *authenticator_current_name(void); #ifdef EXPERIMENTAL_ARC extern gstring *authres_arc(gstring *); #endif -#ifndef DISABLE_DKIM -extern gstring *authres_dkim(gstring *); -#endif extern gstring *authres_smtpauth(gstring *); extern uschar *b64encode(const uschar *, int); @@ -222,13 +220,6 @@ extern void delivery_re_exec(int); extern void die_tainted(const uschar *, const uschar *, int); extern BOOL directory_make(const uschar *, const uschar *, int, BOOL); -#ifndef DISABLE_DKIM -extern uschar *dkim_exim_query_dns_txt(const uschar *); -extern void dkim_exim_sign_init(void); - -extern BOOL dkim_transport_write_message(transport_ctx *, - struct ob_dkim *, const uschar ** errstr); -#endif extern dns_address *dns_address_from_rr(dns_answer *, dns_record *); extern int dns_basic_lookup(dns_answer *, const uschar *, int); extern uschar *dns_build_reverse(const uschar *); @@ -375,6 +366,7 @@ extern int mime_regex(const uschar **, BOOL); extern void mime_set_anomaly(int); #endif +extern gstring *misc_mod_authres(gstring *); extern int misc_mod_conn_init(const uschar *, const uschar *); extern misc_module_info * misc_mod_find(const uschar * modname, uschar **); extern misc_module_info * misc_mod_findonly(const uschar * modname); @@ -552,6 +544,7 @@ extern int smtp_setup_msg(void); extern int smtp_sock_connect(smtp_connect_args *, int, const blob *); extern BOOL smtp_start_session(void); extern int smtp_ungetc(int); +extern void smtp_verify_feed(const uschar *, unsigned); extern BOOL smtp_verify_helo(void); extern int smtp_write_command(void *, int, const char *, ...) PRINTF_FUNCTION(3,4); #ifdef WITH_CONTENT_SCAN @@ -1112,7 +1105,7 @@ g->s = s; static inline gstring * gstring_append(gstring * dest, gstring * item) { -return string_catn(dest, item->s, item->ptr); +return item ? string_catn(dest, item->s, item->ptr) : dest; } diff --git a/src/src/globals.c b/src/src/globals.c index cfa75f1d7..6fae1582f 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -864,25 +864,6 @@ address_item *deliver_recipients = NULL; uschar *deliver_selectstring = NULL; uschar *deliver_selectstring_sender = NULL; -#ifndef DISABLE_DKIM -unsigned dkim_collect_input = 0; -uschar *dkim_cur_signer = NULL; -int dkim_key_length = 0; -void *dkim_signatures = NULL; -uschar *dkim_signers = NULL; -uschar *dkim_signing_domain = NULL; -uschar *dkim_signing_selector = NULL; -gstring *dkim_signing_record = NULL; -uschar *dkim_verify_hashes = US"sha256:sha512"; -uschar *dkim_verify_keytypes = US"ed25519:rsa"; -uschar *dkim_verify_min_keysizes = US"rsa=1024 ed25519=250"; -BOOL dkim_verify_minimal = FALSE; -uschar *dkim_verify_overall = NULL; -uschar *dkim_verify_signers = US"$dkim_signers"; -uschar *dkim_verify_status = NULL; -uschar *dkim_verify_reason = NULL; -#endif - uschar *dns_again_means_nonexist = NULL; int dns_csa_search_limit = 5; int dns_cname_loops = 1; diff --git a/src/src/globals.h b/src/src/globals.h index 8173d771e..1f03cefee 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -545,25 +545,6 @@ extern BOOL disable_fsync; /* Not for normal use */ #endif extern BOOL disable_ipv6; /* Don't do any IPv6 things */ -#ifndef DISABLE_DKIM -extern unsigned dkim_collect_input; /* Runtime count of dkim signtures; tracks whether SMTP input is fed to DKIM validation */ -extern uschar *dkim_cur_signer; /* Expansion variable, holds the current "signer" domain or identity during a acl_smtp_dkim run */ -extern int dkim_key_length; /* Expansion variable, length of signing key in bits */ -extern void *dkim_signatures; /* Actually a (pdkim_signature *) but most files do not need to know */ -extern uschar *dkim_signers; /* Expansion variable, holds colon-separated list of domains and identities that have signed a message */ -extern gstring *dkim_signing_record; /* domains+selectors used */ -extern uschar *dkim_signing_domain; /* Expansion variable, domain used for signing a message. */ -extern uschar *dkim_signing_selector; /* Expansion variable, selector used for signing a message. */ -extern uschar *dkim_verify_hashes; /* Preference order for signatures */ -extern uschar *dkim_verify_keytypes; /* Preference order for signatures */ -extern uschar *dkim_verify_min_keysizes; /* list of minimum key sizes, keyed by algo */ -extern BOOL dkim_verify_minimal; /* Shortcircuit signature verification */ -extern uschar *dkim_verify_overall; /* First successful domain verified, or null */ -extern uschar *dkim_verify_signers; /* Colon-separated list of domains for each of which we call the DKIM ACL */ -extern uschar *dkim_verify_status; /* result for this signature */ -extern uschar *dkim_verify_reason; /* result for this signature */ -#endif - extern uschar *dns_again_means_nonexist; /* Domains that are badly set up */ extern int dns_csa_search_limit; /* How deep to search for CSA SRV records */ extern BOOL dns_csa_use_reverse; /* Check CSA in reverse DNS? (non-standard) */ diff --git a/src/src/hash.c b/src/src/hash.c index 17a52fe43..d629a52bd 100644 --- a/src/src/hash.c +++ b/src/src/hash.c @@ -227,49 +227,6 @@ memcpy(b->data, gcry_md_read(h->sha, 0), h->hashlen); -#elif defined(SHA_POLARSSL) -# define HAVE_PARTIAL_SHA -/******************************************************************************/ - -BOOL -exim_sha_init(hctx * h, hashmethod m) -{ -/*XXX extend for sha512 */ -switch (h->method = m) - { - case HASH_SHA1: h->hashlen = 20; sha1_starts(&h->u.sha1); break; - case HASH_SHA2_256: h->hashlen = 32; sha2_starts(&h->u.sha2, 0); break; - default: h->hashlen = 0; return FALSE; - } -return TRUE; -} - - -void -exim_sha_update(hctx * h, const uschar * data, int len) -{ -switch (h->method) - { - case HASH_SHA1: sha1_update(h->u.sha1, US data, len); break; - case HASH_SHA2_256: sha2_update(h->u.sha2, US data, len); break; - } -} - - -void -exim_sha_finish(hctx * h, blob * b) -{ -b->data = store_get(b->len = h->hashlen, GET_INTAINTED); -switch (h->method) - { - case HASH_SHA1: sha1_finish(h->u.sha1, b->data); break; - case HASH_SHA2_256: sha2_finish(h->u.sha2, b->data); break; - } -} - - - - #elif defined(SHA_NATIVE) /******************************************************************************/ /* Only sha-1 supported */ diff --git a/src/src/hash.h b/src/src/hash.h index 788c9f0ad..9ad837b62 100644 --- a/src/src/hash.h +++ b/src/src/hash.h @@ -19,10 +19,6 @@ # include #elif defined(SHA_GCRYPT) # include -#elif defined(SHA_POLARSSL) -# include "pdkim/pdkim.h" /*XXX ugly */ -# include "pdkim/polarssl/sha1.h" -# include "pdkim/polarssl/sha2.h" #endif @@ -63,12 +59,6 @@ typedef struct { #elif defined(SHA_GCRYPT) gcry_md_hd_t sha; /* Either SHA1 or SHA256 block */ -#elif defined(SHA_POLARSSL) - union { - sha1_context sha1; /* SHA1 block */ - sha2_context sha2; /* SHA256 block */ - } u; - #elif defined(SHA_NATIVE) sha1 sha1; #endif diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile index 3bc535720..d25d5ede0 100644 --- a/src/src/miscmods/Makefile +++ b/src/src/miscmods/Makefile @@ -25,10 +25,28 @@ miscmods.a: $(OBJ) $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c .c.so:; @echo "$(CC) -shared $*.c" - $(FE)$(CC) $(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@ - -dmarc.o dmarc.so: $(HDRS) ../pdkim/pdkim.h dmarc.h dmarc.c + $(FE)$(CC) $(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) \ + -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) \ + $(DLFLAGS) $*.c -o $@ + +# Note that the sources from pdkim/ are linked into the build.../miscmods/ dir +# by scripts/Makelinks. +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 +dmarc.o dmarc.so: $(HDRS) pdkim.h dmarc.h dmarc.c dummy.o: dummy.c -spf.o spf.so: $(HDRS) spf.h spf.c +spf.o spf.so: $(HDRS) spf.h spf.c + +dkim.o: + @echo "$(CC) dkim.c dkim_transport.c pdkim.c signing.c" + $(FE)$(CC) -r $(CFLAGS) $(INCLUDE) \ + dkim.c dkim_transport.c pdkim.c signing.c -o $@ + +dkim.so: + @echo "$(CC) -shared dkim.c dkim_transport.c pdkim.c signing.c" + $(FE)$(CC) $(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) \ + -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) \ + $(DLFLAGS) dkim.c dkim_transport.c pdkim.c signing.c -o $@ # End diff --git a/src/src/miscmods/README b/src/src/miscmods/README index e534a4eed..d1b7a1632 100644 --- a/src/src/miscmods/README +++ b/src/src/miscmods/README @@ -49,6 +49,7 @@ The variables table defins $variables for expansion, using the same definition entry struct as the main var_table in expand.c; entries here should have their proper vtype_ and should be duplicated in the main table but with vtype_module and the module name. +Entries must be in order by the variable name. There are service functions to locate and to locate-or-load modules by name; these hide the static/dynamic aspect of a module. Most @@ -79,6 +80,14 @@ Write an include file with anything callers need to know, in particular and add it to HDRS and PHDRS in OS/Makefile-Base. Add a SUPPORT_ line to Local/Makefile, and (if dynamic) any SUPPORT__INCLUDE or SUPPORT__LIBS required. -Add the capitalised module name to the "miscmods" like in +Add the capitalised module name to the "miscmods" line in scripts/Configure-Makefile. Add all the filenames to the "miscmods" list in scripts/Makelinks + +For statically-linked modules the SUPPORT_ line should say "yes", +for dynamic: "2". + +If include-by-default is wanted for the module, use DISABLE_ instead +of SUPPORT_ (and leave it commented out as appropriate), and prefix +the name in the "miscmods" line with an underbar ("_"). +For dynamic builds, a SUPPORT_ line is still needed. diff --git a/src/src/miscmods/dkim.c b/src/src/miscmods/dkim.c new file mode 100644 index 000000000..38677097b --- /dev/null +++ b/src/src/miscmods/dkim.c @@ -0,0 +1,1379 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2024 */ +/* Copyright (c) University of Cambridge, 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Code for DKIM support. Other DKIM relevant code is in + receive.c, transport.c and transports/smtp.c */ + +#include "../exim.h" + +#ifndef DISABLE_DKIM + +# include "pdkim.h" +# include "signing.h" + +# ifdef MACRO_PREDEF +# include "../macro_predef.h" + +void +params_dkim(void) +{ +builtin_macro_create_var(US"_DKIM_SIGN_HEADERS", US PDKIM_DEFAULT_SIGN_HEADERS); +builtin_macro_create_var(US"_DKIM_OVERSIGN_HEADERS", US PDKIM_OVERSIGN_HEADERS); +} +# else /*!MACRO_PREDEF*/ + +/* Options */ + +uschar *dkim_verify_hashes = US"sha256:sha512"; +uschar *dkim_verify_keytypes = US"ed25519:rsa"; +uschar *dkim_verify_min_keysizes = US"rsa=1024 ed25519=250"; +BOOL dkim_verify_minimal = FALSE; +uschar *dkim_verify_signers = US"$dkim_signers"; + +/* $variables */ + +uschar *dkim_cur_signer = NULL; +int dkim_key_length = 0; +uschar *dkim_signers = NULL; +uschar *dkim_signing_domain = NULL; +uschar *dkim_signing_selector = NULL; +uschar *dkim_verify_reason = NULL; +uschar *dkim_verify_status = NULL; + +/* Working variables */ + +unsigned dkim_collect_input = 0; +void *dkim_signatures = NULL; +gstring *dkim_signing_record = NULL; +uschar *dkim_vdom_firstpass = NULL; + + +extern BOOL dkim_transport_write_message(transport_ctx *, + struct ob_dkim *, const uschar ** errstr); + +/****************************************/ + +pdkim_ctx dkim_sign_ctx; + +int dkim_verify_oldpool; +pdkim_ctx * dkim_verify_ctx = NULL; +pdkim_signature *dkim_cur_sig = NULL; +static const uschar * dkim_collect_error = NULL; + +#define DKIM_MAX_SIGNATURES 20 +static void dkim_exim_verify_pause(BOOL pause); + + +/****************************************/ + +/* Look up the DKIM record in DNS for the given hostname. +Will use the first found if there are multiple. +The return string is tainted, having come from off-site. +*/ + +static uschar * +dkim_exim_query_dns_txt(const uschar * name) +{ +dns_answer * dnsa = store_get_dns_answer(); +dns_scan dnss; +rmark reset_point = store_mark(); +gstring * g = string_get_tainted(256, GET_TAINTED); + +lookup_dnssec_authenticated = NULL; +if (dns_lookup(dnsa, name, T_TXT, NULL) != DNS_SUCCEED) + goto bad; + +/* Search for TXT record */ + +for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); + rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_TXT) + { /* Copy record content to the answer buffer */ + for (int rr_offset = 0; rr_offset < rr->size; ) + { + uschar len = rr->data[rr_offset++]; + + g = string_catn(g, US(rr->data + rr_offset), len); + if (g->ptr >= PDKIM_DNS_TXT_MAX_RECLEN) + goto bad; + + rr_offset += len; + } + + /* Check if this looks like a DKIM record */ + if (Ustrncmp(g->s, "v=", 2) != 0 || strncasecmp(CS g->s, "v=dkim", 6) == 0) + { + store_free_dns_answer(dnsa); + gstring_release_unused(g); + return string_from_gstring(g); + } + + gstring_reset(g); /* overwrite previous record */ + } + +bad: +store_reset(reset_point); +store_free_dns_answer(dnsa); +return NULL; /*XXX better error detail? logging? */ +} + + + +/* Module API: Lookup a DNS DKIM record and parse the pubkey. + +Arguments: + dnsname record to lookup in DNS + pubkey_p pointer for return of pubkey + hashes_p pointer for return of hashes + +Return: srvtype, or NULL on error +*/ + +static const uschar * +dkim_exim_parse_dns_pubkey(const uschar * dnsname, blob ** pubkey_p, + const uschar ** hashes_p) +{ +const uschar * dnstxt = dkim_exim_query_dns_txt(dnsname); +pdkim_pubkey * p; + +if (!dnstxt) + { + DEBUG(D_acl) debug_printf_indent("pubkey dns lookup fail\n"); + return NULL; + } +if (!(p = pdkim_parse_pubkey_record(dnstxt))) + { + DEBUG(D_acl) debug_printf_indent("pubkey dns record format error\n"); + return NULL; + } +*pubkey_p = &p->key; +*hashes_p = p->hashes; +return p->srvtype; +} + + + + +/* Return: + OK verify succesful + FAIL verify did not pass + ERROR problem setting up the pubkey +*/ + +static int +dkim_exim_sig_verify(const blob * sighash, const blob * data_hash, + hashmethod hash, const blob * pubkey, const uschar ** errstr_p) +{ +ev_ctx vctx; +const uschar * errstr; +int rc = OK; + +if ((errstr = exim_dkim_verify_init(pubkey, KEYFMT_DER, &vctx, NULL))) + rc = ERROR; +else if ((errstr = exim_dkim_verify(&vctx, hash, data_hash, sighash))) + rc = FAIL; + +*errstr_p = errstr; +return rc; +} + + + +/****************************************/ + +static BOOL +dkim_exim_init(void * dummy) +{ +if (f.dkim_init_done) return TRUE; +f.dkim_init_done = TRUE; +pdkim_init(); +return TRUE; +} + + + +/* Module API: Set up for verification of a message being received. +Always returns OK. +*/ + +static int +dkim_exim_verify_init(void) +{ +BOOL dot_stuffing = chunking_state <= CHUNKING_OFFERED; + +if (!smtp_input || smtp_batched_input || f.dkim_disable_verify) + return OK; + +dkim_exim_init(NULL); + +/* There is a store-reset between header & body reception for the main pool +(actually, after every header line) so cannot use that as we need the data we +store per-header, during header processing, at the end of body reception +for evaluating the signature. Any allocs done for dkim verify +memory-handling must use a different pool. We use a separate one that we +can reset per message. */ + +dkim_verify_oldpool = store_pool; +store_pool = POOL_MESSAGE; + +/* Free previous context if there is one */ + +if (dkim_verify_ctx) + pdkim_free_ctx(dkim_verify_ctx); + +/* Create new context */ + +dkim_verify_ctx = pdkim_init_verify(&dkim_exim_query_dns_txt, dot_stuffing); +dkim_exim_verify_pause(FALSE); +dkim_collect_input = dkim_verify_ctx ? DKIM_MAX_SIGNATURES : 0; +dkim_collect_error = NULL; + +/* Start feed up with any cached data, but limited to message data */ +receive_get_cache(chunking_state == CHUNKING_LAST + ? chunking_data_left : GETC_BUFFER_UNLIMITED); + +store_pool = dkim_verify_oldpool; +return OK; +} + + +/* Module API : Submit a chunk of data for verification input. +A NULL data pointer indicates end-of-message. +Only use the data when the feed is activated. */ + +static void +dkim_exim_verify_feed(const uschar * data, unsigned len) +{ +int rc; + +store_pool = POOL_MESSAGE; +if ( (dkim_collect_input || !data) + && (rc = pdkim_feed(dkim_verify_ctx, data, len)) != PDKIM_OK) + { + dkim_collect_error = pdkim_errstr(rc); + log_write(0, LOG_MAIN, + "DKIM: validation error: %.100s", dkim_collect_error); + dkim_collect_input = 0; + } +store_pool = dkim_verify_oldpool; +} + + +/* Module API: pause/resume the verification data feed */ + +static void +dkim_exim_verify_pause(BOOL pause) +{ +static unsigned save = 0; +static BOOL paused = FALSE; + +if (!pause) + { + if (paused) + { dkim_collect_input = save; paused = FALSE; } + } +else + if (!paused) + { save = dkim_collect_input; dkim_collect_input = 0; paused = TRUE; } +} + +/* Module API: Finish off the body hashes, calculate sigs and do compares */ + +static void +dkim_exim_verify_finish(void) +{ +int rc; +gstring * g = NULL; +const uschar * errstr = NULL; + +store_pool = POOL_MESSAGE; + +/* Delete eventual previous signature chain */ + +dkim_signers = NULL; +dkim_signatures = NULL; + +if (dkim_collect_error) + { + log_write(0, LOG_MAIN, + "DKIM: Error during validation, disabling signature verification: %.100s", + dkim_collect_error); + f.dkim_disable_verify = TRUE; + goto out; + } + +dkim_collect_input = 0; + +/* Finish DKIM operation and fetch link to signatures chain */ + +rc = pdkim_feed_finish(dkim_verify_ctx, (pdkim_signature **)&dkim_signatures, + &errstr); +if (rc != PDKIM_OK && errstr) + log_write(0, LOG_MAIN, "DKIM: validation error: %s", errstr); + +/* Build a colon-separated list of signing domains (and identities, if present) in dkim_signers */ + +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + { + if (sig->domain) g = string_append_listele(g, ':', sig->domain); + if (sig->identity) g = string_append_listele(g, ':', sig->identity); + } +gstring_release_unused(g); +dkim_signers = string_from_gstring(g); + +out: +store_pool = dkim_verify_oldpool; +} + + + +/* Log the result for the given signature */ +static void +dkim_exim_verify_log_sig(pdkim_signature * sig) +{ +gstring * logmsg; +uschar * s; + +if (!sig) return; + +/* Remember the domain for the first pass result */ + +if ( !dkim_vdom_firstpass + && dkim_verify_status + ? Ustrcmp(dkim_verify_status, US"pass") == 0 + : sig->verify_status == PDKIM_VERIFY_PASS + ) + dkim_vdom_firstpass= string_copy(sig->domain); + +/* Rewrite the sig result if the ACL overrode it. This is only +needed because the DMARC code (sigh) peeks at the dkim sigs. +Mark the sig for this having been done. */ + +if ( dkim_verify_status + && ( dkim_verify_status != dkim_exim_expand_query(DKIM_VERIFY_STATUS) + || dkim_verify_reason != dkim_exim_expand_query(DKIM_VERIFY_REASON) + ) ) + { /* overridden by ACL */ + sig->verify_ext_status = -1; + if (Ustrcmp(dkim_verify_status, US"fail") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_FAIL; + else if (Ustrcmp(dkim_verify_status, US"invalid") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_INVALID; + else if (Ustrcmp(dkim_verify_status, US"none") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_NONE; + else if (Ustrcmp(dkim_verify_status, US"pass") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_PASS; + else + sig->verify_status = -1; + } + +if (!LOGGING(dkim_verbose)) return; + + +logmsg = string_catn(NULL, US"DKIM: ", 6); +if (!(s = sig->domain)) s = US""; +logmsg = string_append(logmsg, 2, "d=", s); +if (!(s = sig->selector)) s = US""; +logmsg = string_append(logmsg, 2, " s=", s); +logmsg = string_fmt_append(logmsg, " c=%s/%s a=%s b=" SIZE_T_FMT, + sig->canon_headers == PDKIM_CANON_SIMPLE ? "simple" : "relaxed", + sig->canon_body == PDKIM_CANON_SIMPLE ? "simple" : "relaxed", + dkim_sig_to_a_tag(sig), + (int)sig->sighash.len > -1 ? sig->sighash.len * 8 : (size_t)0); +if ((s= sig->identity)) logmsg = string_append(logmsg, 2, " i=", s); +if (sig->created > 0) logmsg = string_fmt_append(logmsg, " t=%lu", + sig->created); +if (sig->expires > 0) logmsg = string_fmt_append(logmsg, " x=%lu", + sig->expires); +if (sig->bodylength > -1) logmsg = string_fmt_append(logmsg, " l=%lu", + sig->bodylength); + +if (sig->verify_status & PDKIM_VERIFY_POLICY) + logmsg = string_append(logmsg, 5, + US" [", dkim_verify_status, US" - ", dkim_verify_reason, US"]"); +else + switch (sig->verify_status) + { + case PDKIM_VERIFY_NONE: + logmsg = string_cat(logmsg, US" [not verified]"); + break; + + case PDKIM_VERIFY_INVALID: + logmsg = string_cat(logmsg, US" [invalid - "); + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: + logmsg = string_cat(logmsg, + US"public key record (currently?) unavailable]"); + break; + + case PDKIM_VERIFY_INVALID_BUFFER_SIZE: + logmsg = string_cat(logmsg, US"overlong public key record]"); + break; + + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: + logmsg = string_cat(logmsg, US"syntax error in public key record]"); + break; + + case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: + logmsg = string_cat(logmsg, US"signature tag missing or invalid]"); + break; + + case PDKIM_VERIFY_INVALID_DKIM_VERSION: + logmsg = string_cat(logmsg, US"unsupported DKIM version]"); + break; + + default: + logmsg = string_cat(logmsg, US"unspecified problem]"); + } + break; + + case PDKIM_VERIFY_FAIL: + logmsg = string_cat(logmsg, US" [verification failed - "); + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_FAIL_BODY: + logmsg = string_cat(logmsg, + US"body hash mismatch (body probably modified in transit)]"); + break; + + case PDKIM_VERIFY_FAIL_MESSAGE: + logmsg = string_cat(logmsg, + US"signature did not verify " + "(headers probably modified in transit)]"); + break; + + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: + logmsg = string_cat(logmsg, + US"signature invalid (key too short)]"); + break; + + default: + logmsg = string_cat(logmsg, US"unspecified reason]"); + } + break; + + case PDKIM_VERIFY_PASS: + logmsg = string_cat(logmsg, US" [verification succeeded]"); + break; + } + +log_write(0, LOG_MAIN, "%Y", logmsg); +return; +} + + +/* Module API: Log a line for each signature */ + +void +dkim_exim_verify_log_all(void) +{ +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + dkim_exim_verify_log_sig(sig); +} + + +/* Module API: append a log element with domain for the first passing sig */ + +gstring * +dkim_exim_vdom_firstpass(gstring * g) +{ +if (dkim_vdom_firstpass) + g = string_append(g, 2, US" DKIM=", dkim_vdom_firstpass); +return g; +} + + +/* For one signature, run the DKIM ACL, log the sig result, +and append ths sig status to the status list. + +Args as per dkim_exim_acl_run() below */ + +static int +dkim_acl_call(uschar * id, gstring ** res_ptr, + uschar ** user_msgptr, uschar ** log_msgptr) +{ +int rc; +DEBUG(D_receive) + debug_printf("calling acl_smtp_dkim for identity '%s' domain '%s' sel '%s'\n", + id, dkim_signing_domain, dkim_signing_selector); + +rc = acl_check(ACL_WHERE_DKIM, NULL, acl_smtp_dkim, user_msgptr, log_msgptr); +dkim_exim_verify_log_sig(dkim_cur_sig); +*res_ptr = string_append_listele(*res_ptr, ':', dkim_verify_status); +return rc; +} + + + +/* For the given identity, run the DKIM ACL once for each matching signature. +If none match, run it once. + +Arguments + id Identity to look for in dkim signatures + res_ptr ptr to growable string-list of status results, + appended to per ACL run + user_msgptr where to put a user error (for SMTP response) + log_msgptr where to put a logging message (not for SMTP response) + +Returns: OK access is granted by an ACCEPT verb + DISCARD access is granted by a DISCARD verb + FAIL access is denied + FAIL_DROP access is denied; drop the connection + DEFER can't tell at the moment + ERROR disaster +*/ + +static int +dkim_exim_acl_run(uschar * id, gstring ** res_ptr, + uschar ** user_msgptr, uschar ** log_msgptr) +{ +uschar * cmp_val; +int rc = -1; + +dkim_verify_status = US"none"; +dkim_verify_reason = US""; +dkim_cur_signer = id; + +if (f.dkim_disable_verify || !id || !dkim_verify_ctx) + return OK; + +/* Find signatures to run ACL on */ + +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + if ( (cmp_val = Ustrchr(id, '@') != NULL ? US sig->identity : US sig->domain) + && strcmpic(cmp_val, id) == 0 + ) + { + /* The "dkim_domain" and "dkim_selector" expansion variables have + related globals, since they are used in the signing code too. + Instead of inventing separate names for verification, we set + them here. This is easy since a domain and selector is guaranteed + to be in a signature. The other dkim_* expansion items are + dynamically fetched from dkim_cur_sig at expansion time (see + dkim_exim_expand_query() below). */ + + dkim_cur_sig = sig; + dkim_signing_domain = US sig->domain; + dkim_signing_selector = US sig->selector; + dkim_key_length = sig->keybits; + + /* These two return static strings, so we can compare the addr + later to see if the ACL overwrote them. Check that when logging */ + + dkim_verify_status = dkim_exim_expand_query(DKIM_VERIFY_STATUS); + dkim_verify_reason = dkim_exim_expand_query(DKIM_VERIFY_REASON); + + if ( (rc = dkim_acl_call(id, res_ptr, user_msgptr, log_msgptr)) != OK + || dkim_verify_minimal && Ustrcmp(dkim_verify_status, "pass") == 0) + return rc; + } + +if (rc != -1) + return rc; + +/* No matching sig found. Call ACL once anyway. */ + +dkim_cur_sig = NULL; +return dkim_acl_call(id, res_ptr, user_msgptr, log_msgptr); +} + + +/* Module API: +Loop over dkim_verify_signers option doing ACL calls. If one return any +non-OK value stop and return that, else return OK. +*/ + +int + /*XXX need a user_msgptr */ +dkim_exim_acl_entry(uschar ** user_msgptr, uschar ** log_msgptr) +{ +int rc = OK; + +GET_OPTION("dkim_verify_signers"); +if (dkim_verify_signers && *dkim_verify_signers) + { + const uschar * dkim_verify_signers_expanded = + expand_cstring(dkim_verify_signers); + gstring * results = NULL, * seen_items = NULL; + int signer_sep = 0, old_pool = store_pool; + + if (!dkim_verify_signers_expanded) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "expansion of dkim_verify_signers option failed: %s", + expand_string_message); + return DEFER; + } + + store_pool = POOL_PERM; /* Allow created variables to live to data ACL */ + + /* Loop over signers we want to verify, calling ACL. Default to OK + when no signers are present. Each call from here expands to an ACL + call per matching sig in the message. */ + + for (uschar * item; + item = string_nextinlist(&dkim_verify_signers_expanded, + &signer_sep, NULL, 0); ) + { + /* Prevent running ACL for an empty item */ + if (!item || !*item) continue; + + /* Only run ACL once for each domain or identity, + no matter how often it appears in the expanded list. */ + if (seen_items) + { + uschar * seen_item; + const uschar * seen_items_list = string_from_gstring(seen_items); + int seen_sep = ':'; + BOOL seen_this_item = FALSE; + + while ((seen_item = string_nextinlist(&seen_items_list, &seen_sep, + NULL, 0))) + if (Ustrcmp(seen_item, item) == 0) + { + seen_this_item = TRUE; + break; + } + + if (seen_this_item) + { + DEBUG(D_receive) + debug_printf("acl_smtp_dkim: skipping signer %s, " + "already seen\n", item); + continue; + } + + seen_items = string_catn(seen_items, US":", 1); + } + seen_items = string_cat(seen_items, item); + + if ((rc = dkim_exim_acl_run(item, &results, user_msgptr, log_msgptr)) != OK) + { + DEBUG(D_receive) + debug_printf("acl_smtp_dkim: acl_check returned %d on %s, " + "skipping remaining items\n", rc, item); + break; + } + if (dkim_verify_minimal && Ustrcmp(dkim_verify_status, "pass") == 0) + break; + } /* signers loop */ + + dkim_verify_status = string_from_gstring(results); + store_pool = old_pool; + } +else + dkim_exim_verify_log_all(); + +return rc; +} + +/******************************************************************************/ + +/* Module API */ + +static int +dkim_exim_signer_isinlist(const uschar * l) +{ +return dkim_cur_signer + ? match_isinlist(dkim_cur_signer, &l, 0, NULL, NULL, MCL_STRING, TRUE, NULL) + : FAIL; +} + +/* Module API */ + +static int +dkim_exim_status_listmatch(const uschar * l) +{ /* return good for any match */ +const uschar * s = dkim_verify_status ? dkim_verify_status : US"none"; +int sep = 0, rc = FAIL; +for (uschar * ss; ss = string_nextinlist(&s, &sep, NULL, 0); ) + if ( (rc = match_isinlist(ss, &l, 0, NULL, NULL, MCL_STRING, TRUE, NULL)) + == OK) break; +return rc; +} + +/* Module API: Overwriteable dkim result variables */ + +static void +dkim_exim_setvar(const uschar * name, void * val) +{ +if (Ustrcmp(name, "dkim_verify_status") == 0) + dkim_verify_status = val; +else if (Ustrcmp(name, "dkim_verify_reason") == 0) + dkim_verify_reason = val; +} + +/******************************************************************************/ + +static void +dkim_smtp_reset(void) +{ +dkim_cur_signer = dkim_signers = +dkim_signing_domain = dkim_signing_selector = dkim_signatures = NULL; +f.dkim_disable_verify = FALSE; +dkim_collect_input = 0; +dkim_vdom_firstpass = dkim_verify_status = dkim_verify_reason = NULL; +dkim_key_length = 0; +} + +/******************************************************************************/ + +static uschar * +dkim_exim_expand_defaults(int what) +{ +switch (what) + { + case DKIM_ALGO: return US""; + case DKIM_BODYLENGTH: return US"9999999999999"; + case DKIM_CANON_BODY: return US""; + case DKIM_CANON_HEADERS: return US""; + case DKIM_COPIEDHEADERS: return US""; + case DKIM_CREATED: return US"0"; + case DKIM_EXPIRES: return US"9999999999999"; + case DKIM_HEADERNAMES: return US""; + case DKIM_IDENTITY: return US""; + case DKIM_KEY_GRANULARITY: return US"*"; + case DKIM_KEY_SRVTYPE: return US"*"; + case DKIM_KEY_NOTES: return US""; + case DKIM_KEY_TESTING: return US"0"; + case DKIM_NOSUBDOMAINS: return US"0"; + case DKIM_VERIFY_STATUS: return US"none"; + case DKIM_VERIFY_REASON: return US""; + default: return US""; + } +} + + +/* Module API: return a computed value for a variable expansion */ + +uschar * +dkim_exim_expand_query(int what) +{ +if (!dkim_verify_ctx || f.dkim_disable_verify || !dkim_cur_sig) + return dkim_exim_expand_defaults(what); + +switch (what) + { + case DKIM_ALGO: + return dkim_sig_to_a_tag(dkim_cur_sig); + + case DKIM_BODYLENGTH: + return dkim_cur_sig->bodylength >= 0 + ? string_sprintf("%ld", dkim_cur_sig->bodylength) + : dkim_exim_expand_defaults(what); + + case DKIM_CANON_BODY: + switch (dkim_cur_sig->canon_body) + { + case PDKIM_CANON_RELAXED: return US"relaxed"; + case PDKIM_CANON_SIMPLE: + default: return US"simple"; + } + + case DKIM_CANON_HEADERS: + switch (dkim_cur_sig->canon_headers) + { + case PDKIM_CANON_RELAXED: return US"relaxed"; + case PDKIM_CANON_SIMPLE: + default: return US"simple"; + } + + case DKIM_COPIEDHEADERS: + return dkim_cur_sig->copiedheaders + ? US dkim_cur_sig->copiedheaders : dkim_exim_expand_defaults(what); + + case DKIM_CREATED: + return dkim_cur_sig->created > 0 + ? string_sprintf("%lu", dkim_cur_sig->created) + : dkim_exim_expand_defaults(what); + + case DKIM_EXPIRES: + return dkim_cur_sig->expires > 0 + ? string_sprintf("%lu", dkim_cur_sig->expires) + : dkim_exim_expand_defaults(what); + + case DKIM_HEADERNAMES: + return dkim_cur_sig->headernames + ? dkim_cur_sig->headernames : dkim_exim_expand_defaults(what); + + case DKIM_IDENTITY: + return dkim_cur_sig->identity + ? US dkim_cur_sig->identity : dkim_exim_expand_defaults(what); + + case DKIM_KEY_GRANULARITY: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->granularity + ? US dkim_cur_sig->pubkey->granularity + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_KEY_SRVTYPE: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->srvtype + ? US dkim_cur_sig->pubkey->srvtype + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_KEY_NOTES: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->notes + ? US dkim_cur_sig->pubkey->notes + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_KEY_TESTING: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->testing + ? US"1" + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_NOSUBDOMAINS: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->no_subdomaining + ? US"1" + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_VERIFY_STATUS: + switch (dkim_cur_sig->verify_status) + { + case PDKIM_VERIFY_INVALID: return US"invalid"; + case PDKIM_VERIFY_FAIL: return US"fail"; + case PDKIM_VERIFY_PASS: return US"pass"; + case PDKIM_VERIFY_NONE: + default: return US"none"; + } + + case DKIM_VERIFY_REASON: + switch (dkim_cur_sig->verify_ext_status) + { + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: + return US"pubkey_unavailable"; + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD:return US"pubkey_dns_syntax"; + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return US"pubkey_der_syntax"; + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: return US"pubkey_too_short"; + case PDKIM_VERIFY_FAIL_BODY: return US"bodyhash_mismatch"; + case PDKIM_VERIFY_FAIL_MESSAGE: return US"signature_incorrect"; + } + + default: + return US""; + } +} + + +/* Module API */ + +static void +dkim_exim_sign_init(void) +{ +int old_pool = store_pool; + +dkim_exim_init(NULL); +store_pool = POOL_MAIN; +pdkim_init_context(&dkim_sign_ctx, FALSE, &dkim_exim_query_dns_txt); +store_pool = old_pool; +} + + +/* Generate signatures for the given file. +If a prefix is given, prepend it to the file for the calculations. + +Return: + NULL: error; error string written + string: signature header(s), or a zero-length string (not an error) +*/ + +gstring * +dkim_exim_sign(int fd, off_t off, uschar * prefix, + struct ob_dkim * dkim, const uschar ** errstr) +{ +const uschar * dkim_domain = NULL; +int sep = 0; +gstring * seen_doms = NULL; +pdkim_signature * sig; +gstring * sigbuf; +int pdkim_rc; +int sread; +uschar buf[4096]; +int save_errno = 0; +int old_pool = store_pool; +uschar * errwhen; +const uschar * s; + +if (dkim->dot_stuffed) + dkim_sign_ctx.flags |= PDKIM_DOT_TERM; + +store_pool = POOL_MAIN; + +GET_OPTION("dkim_domain"); +if ((s = dkim->dkim_domain) && !(dkim_domain = expand_cstring(s))) + /* expansion error, do not send message. */ + { errwhen = US"dkim_domain"; goto expand_bad; } + +/* Set $dkim_domain expansion variable to each unique domain in list. */ + +if (dkim_domain) + while ((dkim_signing_domain = string_nextinlist(&dkim_domain, &sep, NULL, 0))) + { + const uschar * dkim_sel; + int sel_sep = 0; + + if (dkim_signing_domain[0] == '\0') + continue; + + /* Only sign once for each domain, no matter how often it + appears in the expanded list. */ + + dkim_signing_domain = string_copylc(dkim_signing_domain); + if (match_isinlist(dkim_signing_domain, CUSS &seen_doms, + 0, NULL, NULL, MCL_STRING, TRUE, NULL) == OK) + continue; + + seen_doms = string_append_listele(seen_doms, ':', dkim_signing_domain); + + /* Set $dkim_selector expansion variable to each selector in list, + for this domain. */ + + GET_OPTION("dkim_selector"); + if (!(dkim_sel = expand_string(dkim->dkim_selector))) + { errwhen = US"dkim_selector"; goto expand_bad; } + + while ((dkim_signing_selector = string_nextinlist(&dkim_sel, &sel_sep, + NULL, 0))) + { + uschar * dkim_canon_expanded; + int pdkim_canon; + uschar * dkim_sign_headers_expanded = NULL; + uschar * dkim_private_key_expanded; + uschar * dkim_hash_expanded; + uschar * dkim_identity_expanded = NULL; + uschar * dkim_timestamps_expanded = NULL; + unsigned long tval = 0, xval = 0; + + /* Get canonicalization to use */ + + GET_OPTION("dkim_canon"); + dkim_canon_expanded = dkim->dkim_canon + ? expand_string(dkim->dkim_canon) : US"relaxed"; + if (!dkim_canon_expanded) /* expansion error, do not send message. */ + { errwhen = US"dkim_canon"; goto expand_bad; } + + if (Ustrcmp(dkim_canon_expanded, "relaxed") == 0) + pdkim_canon = PDKIM_CANON_RELAXED; + else if (Ustrcmp(dkim_canon_expanded, "simple") == 0) + pdkim_canon = PDKIM_CANON_SIMPLE; + else + { + log_write(0, LOG_MAIN, + "DKIM: unknown canonicalization method '%s', defaulting to 'relaxed'.\n", + dkim_canon_expanded); + pdkim_canon = PDKIM_CANON_RELAXED; + } + + GET_OPTION("dkim_sign_headers"); + if ( dkim->dkim_sign_headers + && !(dkim_sign_headers_expanded = expand_string(dkim->dkim_sign_headers))) + { errwhen = US"dkim_sign_header"; goto expand_bad; } + /* else pass NULL, which means default header list */ + + /* Get private key to use. */ + + GET_OPTION("dkim_private_key"); + if (!(dkim_private_key_expanded = expand_string(dkim->dkim_private_key))) + { errwhen = US"dkim_private_key"; goto expand_bad; } + + if ( Ustrlen(dkim_private_key_expanded) == 0 + || Ustrcmp(dkim_private_key_expanded, "0") == 0 + || Ustrcmp(dkim_private_key_expanded, "false") == 0 + ) + continue; /* don't sign, but no error */ + + if ( dkim_private_key_expanded[0] == '/' + && !(dkim_private_key_expanded = + expand_file_big_buffer(dkim_private_key_expanded))) + goto bad; + + GET_OPTION("dkim_hash"); + if (!(dkim_hash_expanded = expand_string(dkim->dkim_hash))) + { errwhen = US"dkim_hash"; goto expand_bad; } + + GET_OPTION("dkim_identity"); + if (dkim->dkim_identity) + if (!(dkim_identity_expanded = expand_string(dkim->dkim_identity))) + { errwhen = US"dkim_identity"; goto expand_bad; } + else if (!*dkim_identity_expanded) + dkim_identity_expanded = NULL; + + GET_OPTION("dkim_timestamps"); + if (dkim->dkim_timestamps) + if (!(dkim_timestamps_expanded = expand_string(dkim->dkim_timestamps))) + { errwhen = US"dkim_timestamps"; goto expand_bad; } + else + { + tval = (unsigned long) time(NULL); + xval = strtoul(CCS dkim_timestamps_expanded, NULL, 10); + if (xval > 0) + xval += tval; + } + + if (!(sig = pdkim_init_sign(&dkim_sign_ctx, dkim_signing_domain, + dkim_signing_selector, + dkim_private_key_expanded, + dkim_hash_expanded, + errstr + ))) + goto bad; + dkim_private_key_expanded[0] = '\0'; + + pdkim_set_optional(sig, + CS dkim_sign_headers_expanded, + CS dkim_identity_expanded, + pdkim_canon, + pdkim_canon, -1, tval, xval); + + if (!pdkim_set_sig_bodyhash(&dkim_sign_ctx, sig)) + goto bad; + + dkim_signing_record = string_append_listele(dkim_signing_record, ':', dkim_signing_domain); + dkim_signing_record = string_append_listele(dkim_signing_record, ':', dkim_signing_selector); + + if (!dkim_sign_ctx.sig) /* link sig to context chain */ + dkim_sign_ctx.sig = sig; + else + { + pdkim_signature * n = dkim_sign_ctx.sig; + while (n->next) n = n->next; + n->next = sig; + } + } + } + +/* We may need to carry on with the data-feed even if there are no DKIM sigs to +produce, if some other package (eg. ARC) is signing. */ + +if (!dkim_sign_ctx.sig && !dkim->force_bodyhash) + { + DEBUG(D_transport) debug_printf("DKIM: no viable signatures to use\n"); + sigbuf = string_get(1); /* return a zero-len string */ + } +else + { + if (prefix && (pdkim_rc = pdkim_feed(&dkim_sign_ctx, prefix, Ustrlen(prefix))) != PDKIM_OK) + goto pk_bad; + + if (lseek(fd, off, SEEK_SET) < 0) + sread = -1; + else + while ((sread = read(fd, &buf, sizeof(buf))) > 0) + if ((pdkim_rc = pdkim_feed(&dkim_sign_ctx, buf, sread)) != PDKIM_OK) + goto pk_bad; + + /* Handle failed read above. */ + if (sread == -1) + { + debug_printf("DKIM: Error reading -K file.\n"); + save_errno = errno; + goto bad; + } + + /* Build string of headers, one per signature */ + + if ((pdkim_rc = pdkim_feed_finish(&dkim_sign_ctx, &sig, errstr)) != PDKIM_OK) + goto pk_bad; + + if (!sig) + { + DEBUG(D_transport) debug_printf("DKIM: no signatures to use\n"); + sigbuf = string_get(1); /* return a zero-len string */ + } + else for (sigbuf = NULL; sig; sig = sig->next) + sigbuf = string_append(sigbuf, 2, US sig->signature_header, US"\r\n"); + } + +CLEANUP: + (void) string_from_gstring(sigbuf); + store_pool = old_pool; + errno = save_errno; + return sigbuf; + +pk_bad: + log_write(0, LOG_MAIN|LOG_PANIC, + "DKIM: signing failed: %.100s", pdkim_errstr(pdkim_rc)); +bad: + sigbuf = NULL; + goto CLEANUP; + +expand_bad: + *errstr = string_sprintf("failed to expand %s: %s", + errwhen, expand_string_message); + log_write(0, LOG_MAIN | LOG_PANIC, "%s", *errstr); + goto bad; +} + + + +#ifdef SUPPORT_DMARC + +/* Module API */ + +static const pdkim_signature * +dkim_sigs_list(void) +{ +return dkim_signatures; +} +#endif + +#ifdef EXPERIMENTAL_ARC + +/* Module API */ +static int +dkim_hashname_to_type(const blob * name) +{ +return pdkim_hashname_to_hashtype(name->data, name->len); +} + +/* Module API */ +hashmethod +dkim_hashtype_to_method(int hashtype) +{ +return hashtype >= 0 ? pdkim_hashes[hashtype].exim_hashmethod : -1; +} + +/* Module API */ +hashmethod +dkim_hashname_to_method(const blob * name) +{ +return dkim_hashtype_to_method(dkim_hashname_to_type(name)); +} + +/* Module API: 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 to use dkim's signing context, else dkim_verify_ctx + canon canonicalization spec, text form + hash hash spec, text form + bodylen byte count for message body + +Return: pointer to hashing method struct +*/ + +static pdkim_bodyhash * +dkim_set_bodyhash(BOOL signing, + const blob * canon, const blob * hashname, long bodylen) +{ +int canon_head = -1, canon_body = -1; + +pdkim_cstring_to_canons(canon->data, canon->len, &canon_head, &canon_body); +return pdkim_set_bodyhash(signing ? &dkim_sign_ctx: dkim_verify_ctx, + dkim_hashname_to_type(hashname), + canon_body, + bodylen); +} + +/* Module API: Sign a blob of data (which might already be a hash, if +Ed25519 or GCrypt signing). + +Arguments: + data to be signed + hm hash to be applied to the data + privkey private key for siging, PEM format + signature pointer for result blob + +Return: NULL, or error string on failure +*/ + +static const uschar * +dkim_sign_blob(const blob * data, hashmethod hm, const uschar * privkey, + blob * signature) +{ +es_ctx sctx; +const uschar * errstr; + +if ((errstr = exim_dkim_signing_init(privkey, &sctx))) + { DEBUG(D_transport) debug_printf("signing key setup: %s\n", errstr); } +else errstr = exim_dkim_sign(&sctx, hm, data, signature); + +return errstr; +} + +#endif /*EXPERIMENTAL_ARC*/ + + +/* Module API */ + +gstring * +authres_dkim(gstring * g) +{ +int start = 0; /* compiler quietening */ + +DEBUG(D_acl) start = gstring_length(g); + +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + { + g = string_catn(g, US";\n\tdkim=", 8); + + if (sig->verify_status & PDKIM_VERIFY_POLICY) + g = string_append(g, 5, + US"policy (", dkim_verify_status, US" - ", dkim_verify_reason, US")"); + else switch(sig->verify_status) + { + case PDKIM_VERIFY_NONE: g = string_cat(g, US"none"); break; + case PDKIM_VERIFY_INVALID: + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: + g = string_cat(g, US"tmperror (pubkey unavailable)\n\t\t"); break; + case PDKIM_VERIFY_INVALID_BUFFER_SIZE: + g = string_cat(g, US"permerror (overlong public key record)\n\t\t"); break; + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: + g = string_cat(g, US"neutral (public key record import problem)\n\t\t"); + break; + case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: + g = string_cat(g, US"neutral (signature tag missing or invalid)\n\t\t"); + break; + case PDKIM_VERIFY_INVALID_DKIM_VERSION: + g = string_cat(g, US"neutral (unsupported DKIM version)\n\t\t"); + break; + default: + g = string_cat(g, US"permerror (unspecified problem)\n\t\t"); break; + } + break; + case PDKIM_VERIFY_FAIL: + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_FAIL_BODY: + g = string_cat(g, + US"fail (body hash mismatch; body probably modified in transit)\n\t\t"); + break; + case PDKIM_VERIFY_FAIL_MESSAGE: + g = string_cat(g, + US"fail (signature did not verify; headers probably modified in transit)\n\t\t"); + break; + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: /* should this really be "polcy"? */ + g = string_fmt_append(g, "fail (public key too short: %u bits)\n\t\t", sig->keybits); + break; + default: + g = string_cat(g, US"fail (unspecified reason)\n\t\t"); + break; + } + break; + case PDKIM_VERIFY_PASS: g = string_cat(g, US"pass"); break; + default: g = string_cat(g, US"permerror"); break; + } + if (sig->domain) g = string_append(g, 2, US" header.d=", sig->domain); + if (sig->identity) g = string_append(g, 2, US" header.i=", sig->identity); + if (sig->selector) g = string_append(g, 2, US" header.s=", sig->selector); + g = string_append(g, 2, US" header.a=", dkim_sig_to_a_tag(sig)); + } + +DEBUG(D_acl) + if (gstring_length(g) == start) + debug_printf("DKIM:\tno authres\n"); + else + debug_printf("DKIM:\tauthres '%.*s'\n", g->ptr - start - 3, g->s + start + 3); +return g; +} + +/******************************************************************************/ +/* Module API */ + +static optionlist dkim_options[] = { + { "acl_smtp_dkim", opt_stringptr, {&acl_smtp_dkim} }, + { "dkim_verify_hashes", opt_stringptr, {&dkim_verify_hashes} }, + { "dkim_verify_keytypes", opt_stringptr, {&dkim_verify_keytypes} }, + { "dkim_verify_min_keysizes", opt_stringptr, {&dkim_verify_min_keysizes} }, + { "dkim_verify_minimal", opt_bool, {&dkim_verify_minimal} }, + { "dkim_verify_signers", opt_stringptr, {&dkim_verify_signers} }, +}; + +static void * dkim_functions[] = { + [DKIM_VERIFY_FEED] = dkim_exim_verify_feed, + [DKIM_VERIFY_PAUSE] = dkim_exim_verify_pause, + [DKIM_VERIFY_FINISH] = dkim_exim_verify_finish, + [DKIM_ACL_ENTRY] = dkim_exim_acl_entry, + [DKIM_VERIFY_LOG_ALL] = dkim_exim_verify_log_all, + [DKIM_VDOM_FIRSTPASS] = dkim_exim_vdom_firstpass, + + [DKIM_SIGNER_ISINLIST] = dkim_exim_signer_isinlist, + [DKIM_STATUS_LISTMATCH] = dkim_exim_status_listmatch, + + [DKIM_SETVAR] = dkim_exim_setvar, + [DKIM_EXPAND_QUERY] = dkim_exim_expand_query, + + [DKIM_TRANSPORT_INIT] = dkim_exim_sign_init, + [DKIM_TRANSPORT_WRITE] = dkim_transport_write_message, + +#ifdef SUPPORT_DMARC + [DKIM_SIGS_LIST] = dkim_sigs_list, +#endif +#ifdef EXPERIMENTAL_ARC + [DKIM_HASHNAME_TO_TYPE] = dkim_hashname_to_type, + [DKIM_HASHTYPE_TO_METHOD] = dkim_hashtype_to_method, + [DKIM_HASHNAME_TO_METHOD] = dkim_hashname_to_method, + [DKIM_SET_BODYHASH] = dkim_set_bodyhash, + [DKIM_DNS_PUBKEY] = dkim_exim_parse_dns_pubkey, + [DKIM_SIG_VERIFY] = dkim_exim_sig_verify, + [DKIM_HEADER_RELAX] = pdkim_relax_header_n, + [DKIM_SIGN_DATA] = dkim_sign_blob, +#endif +}; + +static var_entry dkim_variables[] = { + { "dkim_algo", vtype_dkim, (void *)DKIM_ALGO }, + { "dkim_bodylength", vtype_dkim, (void *)DKIM_BODYLENGTH }, + { "dkim_canon_body", vtype_dkim, (void *)DKIM_CANON_BODY }, + { "dkim_canon_headers", vtype_dkim, (void *)DKIM_CANON_HEADERS }, + { "dkim_copiedheaders", vtype_dkim, (void *)DKIM_COPIEDHEADERS }, + { "dkim_created", vtype_dkim, (void *)DKIM_CREATED }, + { "dkim_cur_signer", vtype_stringptr, &dkim_cur_signer }, + { "dkim_domain", vtype_stringptr, &dkim_signing_domain }, + { "dkim_expires", vtype_dkim, (void *)DKIM_EXPIRES }, + { "dkim_headernames", vtype_dkim, (void *)DKIM_HEADERNAMES }, + { "dkim_identity", vtype_dkim, (void *)DKIM_IDENTITY }, + { "dkim_key_granularity",vtype_dkim, (void *)DKIM_KEY_GRANULARITY }, + { "dkim_key_length", vtype_int, &dkim_key_length }, + { "dkim_key_nosubdomains",vtype_dkim, (void *)DKIM_NOSUBDOMAINS }, + { "dkim_key_notes", vtype_dkim, (void *)DKIM_KEY_NOTES }, + { "dkim_key_srvtype", vtype_dkim, (void *)DKIM_KEY_SRVTYPE }, + { "dkim_key_testing", vtype_dkim, (void *)DKIM_KEY_TESTING }, + { "dkim_selector", vtype_stringptr, &dkim_signing_selector }, + { "dkim_signers", vtype_stringptr, &dkim_signers }, + { "dkim_verify_reason", vtype_stringptr, &dkim_verify_reason }, + { "dkim_verify_status", vtype_stringptr, &dkim_verify_status }, +}; + +misc_module_info dkim_module_info = { + .name = US"dkim", +# if SUPPORT_DKIM==2 + .dyn_magic = MISC_MODULE_MAGIC, +# endif + .init = dkim_exim_init, + .msg_init = dkim_exim_verify_init, + .authres = authres_dkim, + .smtp_reset = dkim_smtp_reset, + + .options = dkim_options, + .options_count = nelem(dkim_options), + + .functions = dkim_functions, + .functions_count = nelem(dkim_functions), + + .variables = dkim_variables, + .variables_count = nelem(dkim_variables), +}; + +# endif /*!MACRO_PREDEF*/ +#endif /*!DISABLE_DKIM*/ diff --git a/src/src/miscmods/dkim.h b/src/src/miscmods/dkim.h new file mode 100644 index 000000000..aa14d58d2 --- /dev/null +++ b/src/src/miscmods/dkim.h @@ -0,0 +1,47 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge, 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +gstring * dkim_exim_sign(int, off_t, uschar *, struct ob_dkim *, const uschar **); +uschar *dkim_exim_expand_query(int); + + +#define DKIM_ALGO 1 +#define DKIM_BODYLENGTH 2 +#define DKIM_CANON_BODY 3 +#define DKIM_CANON_HEADERS 4 +#define DKIM_COPIEDHEADERS 5 +#define DKIM_CREATED 6 +#define DKIM_EXPIRES 7 +#define DKIM_HEADERNAMES 8 +#define DKIM_IDENTITY 9 +#define DKIM_KEY_GRANULARITY 10 +#define DKIM_KEY_SRVTYPE 11 +#define DKIM_KEY_NOTES 12 +#define DKIM_KEY_TESTING 13 +#define DKIM_NOSUBDOMAINS 14 +#define DKIM_VERIFY_STATUS 15 +#define DKIM_VERIFY_REASON 16 + + +extern unsigned dkim_collect_input; /* Runtime count of dkim signtures; tracks whether SMTP input is fed to DKIM validation */ +extern uschar *dkim_cur_signer; /* Expansion variable, holds the current "signer" domain or identity during a acl_smtp_dkim run */ +extern int dkim_key_length; /* Expansion variable, length of signing key in bits */ +extern void *dkim_signatures; /* Actually a (pdkim_signature *) but most files do not need to know */ +extern uschar *dkim_signers; /* Expansion variable, holds colon-separated list of domains and identities that have signed a message */ +extern gstring *dkim_signing_record; /* domains+selectors used */ +extern uschar *dkim_signing_domain; /* Expansion variable, domain used for signing a message. */ +extern uschar *dkim_signing_selector; /* Expansion variable, selector used for signing a message. */ +extern uschar *dkim_verify_hashes; /* Preference order for signatures */ +extern uschar *dkim_verify_keytypes; /* Preference order for signatures */ +extern uschar *dkim_verify_min_keysizes; /* list of minimum key sizes, keyed by algo */ +extern BOOL dkim_verify_minimal; /* Shortcircuit signature verification */ +extern uschar *dkim_vdom_firstpass; /* First successful domain verified, or null */ +extern uschar *dkim_verify_signers; /* Colon-separated list of domains for each of which we call the DKIM ACL */ +extern uschar *dkim_verify_status; /* result for this signature */ +extern uschar *dkim_verify_reason; /* result for this signature */ + diff --git a/src/src/miscmods/dkim_api.h b/src/src/miscmods/dkim_api.h new file mode 100644 index 000000000..54e1141ff --- /dev/null +++ b/src/src/miscmods/dkim_api.h @@ -0,0 +1,36 @@ +/************************************************* +* 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 dkim module */ + + +/* Function table entry numbers */ + +#define DKIM_VERIFY_FEED 0 +#define DKIM_VERIFY_PAUSE 1 +#define DKIM_VERIFY_FINISH 2 +#define DKIM_ACL_ENTRY 3 +#define DKIM_VERIFY_LOG_ALL 4 +#define DKIM_VDOM_FIRSTPASS 5 +#define DKIM_SIGNER_ISINLIST 6 +#define DKIM_STATUS_LISTMATCH 7 +#define DKIM_SETVAR 8 +#define DKIM_EXPAND_QUERY 9 +#define DKIM_TRANSPORT_INIT 10 +#define DKIM_TRANSPORT_WRITE 11 + +#define DKIM_SIGS_LIST 12 + +#define DKIM_HASHNAME_TO_TYPE 13 +#define DKIM_HASHTYPE_TO_METHOD 14 +#define DKIM_HASHNAME_TO_METHOD 15 +#define DKIM_SET_BODYHASH 16 +#define DKIM_DNS_PUBKEY 17 +#define DKIM_SIG_VERIFY 18 +#define DKIM_HEADER_RELAX 19 +#define DKIM_SIGN_DATA 20 diff --git a/src/src/miscmods/dkim_transport.c b/src/src/miscmods/dkim_transport.c new file mode 100644 index 000000000..0500da2be --- /dev/null +++ b/src/src/miscmods/dkim_transport.c @@ -0,0 +1,421 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 - 2024 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Transport shim for dkim signing */ + + +#include "../exim.h" + +#ifndef DISABLE_DKIM /* rest of file */ + + +static BOOL +dkt_sign_fail(struct ob_dkim * dkim, int * errp) +{ +GET_OPTION("dkim_strict"); +if (dkim->dkim_strict) + { + uschar * dkim_strict_result = expand_string(dkim->dkim_strict); + + if (dkim_strict_result) + if ( strcmpic(dkim_strict_result, US"1") == 0 + || strcmpic(dkim_strict_result, US"true") == 0) + { + /* Set errno to something halfway meaningful */ + *errp = EACCES; + log_write(0, LOG_MAIN, "DKIM: message could not be signed," + " and dkim_strict is set. Deferring message delivery."); + return FALSE; + } + } +return TRUE; +} + +/* Send the file at in_fd down the output fd */ + +static BOOL +dkt_send_file(int out_fd, int in_fd, off_t off +#ifdef OS_SENDFILE + , size_t size +#endif + ) +{ +#ifdef OS_SENDFILE +DEBUG(D_transport) debug_printf("send file fd=%d size=%u\n", out_fd, (unsigned)(size - off)); +#else +DEBUG(D_transport) debug_printf("send file fd=%d\n", out_fd); +#endif + +/*XXX should implement timeout, like transport_write_block_fd() ? */ + +#ifdef OS_SENDFILE +/* We can use sendfile() to shove the file contents + to the socket. However only if we don't use TLS, + as then there's another layer of indirection + before the data finally hits the socket. */ +if (tls_out.active.sock != out_fd) + { + ssize_t copied = 0; + + while(copied >= 0 && off < size) + copied = os_sendfile(out_fd, in_fd, &off, size - off); + if (copied < 0) + return FALSE; + } +else + +#endif + + { + int sread, wwritten; + + /* Rewind file */ + if (lseek(in_fd, off, SEEK_SET) < 0) return FALSE; + + /* Send file down the original fd */ + while((sread = read(in_fd, deliver_out_buffer, DELIVER_OUT_BUFFER_SIZE)) > 0) + { + uschar * p = deliver_out_buffer; + /* write the chunk */ + + while (sread) + { +#ifndef DISABLE_TLS + wwritten = tls_out.active.sock == out_fd + ? tls_write(tls_out.active.tls_ctx, p, sread, FALSE) + : write(out_fd, CS p, sread); +#else + wwritten = write(out_fd, CS p, sread); +#endif + if (wwritten == -1) + return FALSE; + p += wwritten; + sread -= wwritten; + } + } + + if (sread == -1) + return FALSE; + } + +return TRUE; +} + + + + +/* This function is a wrapper around transport_write_message(). + It is only called from the smtp transport if DKIM or Domainkeys support + is active and no transport filter is to be used. + +Arguments: + As for transport_write_message() in transort.c, with additional arguments + for DKIM. + +Returns: TRUE on success; FALSE (with errno) for any failure +*/ + +static BOOL +dkt_direct(transport_ctx * tctx, struct ob_dkim * dkim, + const uschar ** err) +{ +int save_fd = tctx->u.fd; +int save_options = tctx->options; +BOOL save_wireformat = f.spool_file_wireformat; +uschar * hdrs; +gstring * dkim_signature; +int hsize; +const uschar * errstr; +BOOL rc; + +DEBUG(D_transport) debug_printf("dkim signing direct-mode\n"); + +/* Get headers in string for signing and transmission. Do CRLF +and dotstuffing (but no body nor dot-termination) */ + +tctx->u.msg = NULL; +tctx->options = tctx->options & ~(topt_end_dot | topt_use_bdat) + | topt_output_string | topt_no_body; + +rc = transport_write_message(tctx, 0); +hdrs = string_from_gstring(tctx->u.msg); +hsize = tctx->u.msg->ptr; + +tctx->u.fd = save_fd; +tctx->options = save_options; +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. */ + +dkim->dot_stuffed = f.spool_file_wireformat; +if (!(dkim_signature = dkim_exim_sign(deliver_datafile, + spool_data_start_offset(message_id), hdrs, dkim, &errstr))) + if (!(rc = dkt_sign_fail(dkim, &errno))) + { + *err = errstr; + return FALSE; + } + +#ifdef EXPERIMENTAL_ARC +if (dkim->arc_signspec) /* Prepend ARC headers */ + { + uschar * e = NULL; + if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, &e))) + { + *err = e; + return FALSE; + } + } +#endif + +/* Write the signature and headers into the deliver-out-buffer. This should +mean they go out in the same packet as the MAIL, RCPT and (first) BDAT commands +(transport_write_message() sizes the BDAT for the buffered amount) - for short +messages, the BDAT LAST command. We want no dotstuffing expansion here, it +having already been done - but we have to say we want CRLF output format, and +temporarily set the marker for possible already-CRLF input. */ + +tctx->options &= ~topt_escape_headers; +f.spool_file_wireformat = TRUE; +transport_write_reset(0); +if ( ( dkim_signature + && dkim_signature->ptr > 0 + && !write_chunk(tctx, dkim_signature->s, dkim_signature->ptr) + ) + || !write_chunk(tctx, hdrs, hsize) + ) + return FALSE; + +f.spool_file_wireformat = save_wireformat; +tctx->options = save_options | topt_no_headers | topt_continuation; + +if (!(transport_write_message(tctx, 0))) + return FALSE; + +tctx->options = save_options; +return TRUE; +} + + +/* This function is a wrapper around transport_write_message(). + It is only called from the smtp transport if DKIM or Domainkeys support + is active and a transport filter is to be used. The function sets up a + replacement fd into a -K file, then calls the normal function. This way, the + exact bits that exim would have put "on the wire" will end up in the file + (except for TLS encapsulation, which is the very very last thing). When we + are done signing the file, send the signed message down the original fd (or + TLS fd). + +Arguments: + As for transport_write_message() in transort.c, with additional arguments + for DKIM. + +Returns: TRUE on success; FALSE (with errno) for any failure +*/ + +static BOOL +dkt_via_kfile(transport_ctx * tctx, struct ob_dkim * dkim, const uschar ** err) +{ +int dkim_fd; +int save_errno = 0; +BOOL rc; +uschar * dkim_spool_name; +gstring * dkim_signature; +int options, dlen; +off_t k_file_size; +const uschar * errstr; + +dkim_spool_name = spool_fname(US"input", message_subdir, message_id, + string_sprintf("-%d-K", (int)getpid())); + +DEBUG(D_transport) debug_printf("dkim signing via file %s\n", dkim_spool_name); + +if ((dkim_fd = Uopen(dkim_spool_name, O_RDWR|O_CREAT|O_TRUNC, SPOOL_MODE)) < 0) + { + /* Can't create spool file. Ugh. */ + rc = FALSE; + save_errno = errno; + *err = string_sprintf("dkim spoolfile create: %s", strerror(errno)); + goto CLEANUP; + } + +/* Call transport utility function to write the -K file; does the CRLF expansion +(but, in the CHUNKING case, neither dot-stuffing nor dot-termination). */ + + { + int save_fd = tctx->u.fd; + tctx->u.fd = dkim_fd; + options = tctx->options; + tctx->options &= ~topt_use_bdat; + + rc = transport_write_message(tctx, 0); + + tctx->u.fd = save_fd; + tctx->options = options; + } + +/* Save error state. We must clean up before returning. */ +if (!rc) + { + save_errno = errno; + 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. */ + +dkim->dot_stuffed = !!(options & topt_end_dot); +if (!(dkim_signature = dkim_exim_sign(dkim_fd, 0, NULL, dkim, &errstr))) + { + dlen = 0; + if (!(rc = dkt_sign_fail(dkim, &save_errno))) + { + *err = errstr; + goto CLEANUP; + } + } +else + dlen = dkim_signature->ptr; + +#ifdef EXPERIMENTAL_ARC +if (dkim->arc_signspec) /* Prepend ARC headers */ + { + if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, USS err))) + goto CLEANUP; + dlen = dkim_signature->ptr; + } +#endif + +#ifndef OS_SENDFILE +if (options & topt_use_bdat) +#endif + if ((k_file_size = lseek(dkim_fd, 0, SEEK_END)) < 0) + { + *err = string_sprintf("dkim spoolfile seek: %s", strerror(errno)); + goto CLEANUP; + } + +if (options & topt_use_bdat) + { + /* On big messages output a precursor chunk to get any pipelined + MAIL & RCPT commands flushed, then reap the responses so we can + error out on RCPT rejects before sending megabytes. */ + + if ( dlen + k_file_size > DELIVER_OUT_BUFFER_SIZE + && dlen > 0) + { + if ( tctx->chunk_cb(tctx, dlen, 0) != OK + || !transport_write_block(tctx, + dkim_signature->s, dlen, FALSE) + || tctx->chunk_cb(tctx, 0, tc_reap_prev) != OK + ) + goto err; + dlen = 0; + } + + /* Send the BDAT command for the entire message, as a single LAST-marked + chunk. */ + + if (tctx->chunk_cb(tctx, dlen + k_file_size, tc_chunk_last) != OK) + goto err; + } + +if(dlen > 0 && !transport_write_block(tctx, dkim_signature->s, dlen, TRUE)) + goto err; + +if (!dkt_send_file(tctx->u.fd, dkim_fd, 0 +#ifdef OS_SENDFILE + , k_file_size +#endif + )) + { + save_errno = errno; + rc = FALSE; + } + +CLEANUP: + /* unlink -K file */ + if (dkim_fd >= 0) (void)close(dkim_fd); + Uunlink(dkim_spool_name); + errno = save_errno; + return rc; + +err: + save_errno = errno; + rc = FALSE; + goto CLEANUP; +} + + + +/*************************************************************************************************** +* External interface to write the message, while signing it with DKIM and/or Domainkeys * +***************************************************************************************************/ + +/* This function is a wrapper around transport_write_message(). + It is only called from the smtp transport if DKIM or Domainkeys support + is compiled in. + +Arguments: + As for transport_write_message() in transort.c, with additional arguments + for DKIM. + +Returns: TRUE on success; FALSE (with errno) for any failure +*/ + +BOOL +dkim_transport_write_message(transport_ctx * tctx, + struct ob_dkim * dkim, const uschar ** err) +{ +BOOL yield; + +/* If we can't sign, just call the original function. */ + +if ( !(dkim->dkim_private_key && dkim->dkim_domain && dkim->dkim_selector) + && !dkim->force_bodyhash) + return transport_write_message(tctx, 0); + +/* If there is no filter command set up, construct the message and calculate +a dkim signature of it, send the signature and a reconstructed message. This +avoids using a temporary file. */ + +if ( !transport_filter_argv + || !*transport_filter_argv + || !**transport_filter_argv + ) + yield = dkt_direct(tctx, dkim, err); + +else + /* Use the transport path to write a file, calculate a dkim signature, + send the signature and then send the file. */ + + yield = dkt_via_kfile(tctx, dkim, err); + +tctx->addr->dkim_used = string_from_gstring(dkim_signing_record); +return yield; +} + +#endif /* whole file */ + +/* vi: aw ai sw=2 +*/ +/* End of dkim_transport.c */ diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c index 37648d045..4a8beab66 100644 --- a/src/src/miscmods/dmarc.c +++ b/src/src/miscmods/dmarc.c @@ -22,7 +22,7 @@ # include "../functions.h" # include "dmarc.h" -# include "../pdkim/pdkim.h" +# include "pdkim.h" OPENDMARC_LIB_T dmarc_ctx; DMARC_POLICY_T *dmarc_pctx = NULL; @@ -32,7 +32,8 @@ BOOL dmarc_abort = FALSE; uschar *dmarc_pass_fail = US"skipped"; header_line *from_header = NULL; -misc_module_info * spf_mod_info; +static misc_module_info * dmarc_dkim_mod_info; +static misc_module_info * dmarc_spf_mod_info; SPF_response_t *spf_response_p; int dmarc_spf_ares_result = 0; uschar *spf_sender_domain = NULL; @@ -73,9 +74,19 @@ static BOOL dmarc_init(void *) { uschar * errstr; -if (!(spf_mod_info = misc_mod_find(US"spf", &errstr))) - log_write(0, LOG_MAIN|LOG_PANIC_DIE, - "dmarc: failed to find SPF module: %s", errstr); +if (!(dmarc_spf_mod_info = misc_mod_find(US"spf", &errstr))) + { + log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr); + return FALSE; + } + +/*XXX not yet used, but will be */ +if (!(dmarc_dkim_mod_info = misc_mod_find(US"dkim", &errstr))) + { + log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr); + return FALSE; + } + return TRUE; } @@ -188,6 +199,8 @@ return OK; static void dmarc_smtp_reset(void) { +f.dmarc_has_been_checked = f.dmarc_disable_verify = +f.dmarc_enable_forensic = FALSE; dmarc_domain_policy = dmarc_status = dmarc_status_text = dmarc_used_domain = NULL; } @@ -394,7 +407,6 @@ dmarc_process(void) int sr, origin; /* used in SPF section */ int dmarc_spf_result = 0; /* stores spf into dmarc conn ctx */ int tmp_ans, c; -pdkim_signature * sig = dkim_signatures; uschar * rr; BOOL has_dmarc_record = TRUE; u_char ** ruf; /* forensic report addressees, if called for */ @@ -453,6 +465,7 @@ if (!dmarc_abort && !sender_host_authenticated) { uschar * dmarc_domain; gstring * dkim_history_buffer = NULL; + typedef const pdkim_signature * (*sigs_fn_t)(void); /* Use the envelope sender domain for this part of DMARC */ @@ -460,9 +473,9 @@ if (!dmarc_abort && !sender_host_authenticated) { typedef SPF_response_t * (*fn_t)(void); - if (spf_mod_info) + if (dmarc_spf_mod_info) /*XXX ugly use of a pointer */ - spf_response_p = ((fn_t *) spf_mod_info->functions)[SPF_GET_RESPONSE](); + spf_response_p = ((fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESPONSE](); } if (!spf_response_p) @@ -519,7 +532,9 @@ if (!dmarc_abort && !sender_host_authenticated) /* Now we cycle through the dkim signature results and put into the opendmarc context, further building the DMARC reply. */ - for(pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + for(const pdkim_signature * sig = + (((sigs_fn_t *)dmarc_dkim_mod_info->functions)[DKIM_SIGS_LIST])(); + sig; sig = sig->next) { int dkim_result, dkim_ares_result, vs, ves; @@ -749,7 +764,6 @@ static optionlist dmarc_options[] = { static void * dmarc_functions[] = { [DMARC_PROCESS] = dmarc_process, [DMARC_EXPAND_QUERY] = dmarc_exim_expand_query, - [DMARC_AUTHRES] = authres_dmarc, [DMARC_STORE_DATA] = dmarc_store_data, }; @@ -775,6 +789,7 @@ misc_module_info dmarc_module_info = .lib_vers_report = dmarc_version_report, .smtp_reset = dmarc_smtp_reset, .msg_init = dmarc_msg_init, + .authres = authres_dmarc, .options = dmarc_options, .options_count = nelem(dmarc_options), diff --git a/src/src/miscmods/dmarc_api.h b/src/src/miscmods/dmarc_api.h index 6ba8a5060..9d9ef62da 100644 --- a/src/src/miscmods/dmarc_api.h +++ b/src/src/miscmods/dmarc_api.h @@ -13,5 +13,4 @@ #define DMARC_PROCESS 0 #define DMARC_EXPAND_QUERY 1 -#define DMARC_AUTHRES 2 -#define DMARC_STORE_DATA 3 +#define DMARC_STORE_DATA 2 diff --git a/src/src/miscmods/pdkim/Makefile b/src/src/miscmods/pdkim/Makefile new file mode 100644 index 000000000..47f92eed4 --- /dev/null +++ b/src/src/miscmods/pdkim/Makefile @@ -0,0 +1,19 @@ +# Make file for building the pdkim library. +# Copyright (c) The Exim Maintainers 1995 - 2018 + +OBJ = pdkim.o signing.o + +pdkim.a: $(OBJ) + @$(RM_COMMAND) -f pdkim.a + @echo "$(AR) pdkim.a" + $(FE)$(AR) pdkim.a $(OBJ) + $(RANLIB) $@ + +.SUFFIXES: .o .c +.c.o:; @echo "$(CC) $*.c" + $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -I. $*.c + +pdkim.o: $(HDRS) crypt_ver.h pdkim.h pdkim.c +signing.o: $(HDRS) crypt_ver.h signing.h signing.c + +# End diff --git a/src/src/miscmods/pdkim/README b/src/src/miscmods/pdkim/README new file mode 100644 index 000000000..953e86eae --- /dev/null +++ b/src/src/miscmods/pdkim/README @@ -0,0 +1,9 @@ +PDKIM - a RFC4871 (DKIM) implementation +http://duncanthrax.net/pdkim/ +Copyright (C) 2009 Tom Kistner + +No longer includes code from the PolarSSL project. +Copyright (C) 2016 Jeremy Harris + +This copy of PDKIM is included with Exim. For a standalone distribution, +visit http://duncanthrax.net/pdkim/. diff --git a/src/src/miscmods/pdkim/crypt_ver.h b/src/src/miscmods/pdkim/crypt_ver.h new file mode 100644 index 000000000..56ae236c1 --- /dev/null +++ b/src/src/miscmods/pdkim/crypt_ver.h @@ -0,0 +1,34 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Jeremy Harris 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Signing and hashing routine selection for PDKIM */ + +#include "../exim.h" +#include "../sha_ver.h" + + +#ifdef USE_GNUTLS +# include + +# if GNUTLS_VERSION_NUMBER >= 0x030000 +# define SIGN_GNUTLS +# if GNUTLS_VERSION_NUMBER >= 0x030600 +# define SIGN_HAVE_ED25519 +# endif +# else +# define SIGN_GCRYPT +# endif +#endif + +#ifdef USE_OPENSSL +# define SIGN_OPENSSL +# if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10101000L +# define SIGN_HAVE_ED25519 +# endif +#endif + diff --git a/src/src/miscmods/pdkim/pdkim.c b/src/src/miscmods/pdkim/pdkim.c new file mode 100644 index 000000000..cdbdfc5e0 --- /dev/null +++ b/src/src/miscmods/pdkim/pdkim.c @@ -0,0 +1,2088 @@ +/* + * PDKIM - a RFC4871 (DKIM) implementation + * + * Copyright (c) The Exim Maintainers 2021 - 2024 + * Copyright (C) 2016 - 2020 Jeremy Harris + * Copyright (C) 2009 - 2016 Tom Kistner + * SPDX-License-Identifier: GPL-2.0-or-later + * + * http://duncanthrax.net/pdkim/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "../exim.h" + + +#ifndef DISABLE_DKIM /* entire file */ + +#ifdef DISABLE_TLS +# error Must not DISABLE_TLS, for DKIM +#endif + +#include "crypt_ver.h" + +#ifdef SIGN_OPENSSL +# include +# include +# include +#elif defined(SIGN_GNUTLS) +# include +# include +#endif + +#include "pdkim.h" +#include "signing.h" + +#define PDKIM_SIGNATURE_VERSION "1" +#define PDKIM_PUB_RECORD_VERSION US "DKIM1" + +#define PDKIM_MAX_HEADER_LEN 65536 +#define PDKIM_MAX_HEADERS 512 +#define PDKIM_MAX_BODY_LINE_LEN 16384 +#define PDKIM_DNS_TXT_MAX_NAMELEN 1024 + +/* -------------------------------------------------------------------------- */ +struct pdkim_stringlist { + uschar * value; + int tag; + void * next; +}; + +/* -------------------------------------------------------------------------- */ +/* A bunch of list constants */ +const uschar * pdkim_querymethods[] = { + US"dns/txt", + NULL +}; +const uschar * pdkim_canons[] = { + US"simple", + US"relaxed", + NULL +}; + +const pdkim_hashtype pdkim_hashes[] = { + { US"sha1", HASH_SHA1 }, + { US"sha256", HASH_SHA2_256 }, + { US"sha512", HASH_SHA2_512 } +}; + +const uschar * pdkim_keytypes[] = { + [KEYTYPE_RSA] = US"rsa", +#ifdef SIGN_HAVE_ED25519 + [KEYTYPE_ED25519] = US"ed25519", /* Works for 3.6.0 GnuTLS, OpenSSL 1.1.1 */ +#endif + +#ifdef notyet_EC_dkim_extensions /* https://tools.ietf.org/html/draft-srose-dkim-ecc-00 */ + US"eccp256", + US"eccp348", + US"ed448", +#endif +}; + +typedef struct pdkim_combined_canon_entry { + const uschar * str; + int canon_headers; + int canon_body; +} pdkim_combined_canon_entry; + +pdkim_combined_canon_entry pdkim_combined_canons[] = { + { US"simple/simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, + { US"simple/relaxed", PDKIM_CANON_SIMPLE, PDKIM_CANON_RELAXED }, + { US"relaxed/simple", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, + { US"relaxed/relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_RELAXED }, + { US"simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, + { US"relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, + { NULL, 0, 0 } +}; + + +static const blob lineending = {.data = US"\r\n", .len = 2}; + +/* -------------------------------------------------------------------------- */ +uschar * +dkim_sig_to_a_tag(const pdkim_signature * sig) +{ +if ( sig->keytype < 0 || sig->keytype > nelem(pdkim_keytypes) + || sig->hashtype < 0 || sig->hashtype > nelem(pdkim_hashes)) + return US"err"; +return string_sprintf("%s-%s", + pdkim_keytypes[sig->keytype], pdkim_hashes[sig->hashtype].dkim_hashname); +} + + +static int +pdkim_keyname_to_keytype(const uschar * s) +{ +for (int i = 0; i < nelem(pdkim_keytypes); i++) + if (Ustrcmp(s, pdkim_keytypes[i]) == 0) return i; +return -1; +} + +int +pdkim_hashname_to_hashtype(const uschar * s, unsigned len) +{ +if (!len) len = Ustrlen(s); +for (int i = 0; i < nelem(pdkim_hashes); i++) + if (Ustrncmp(s, pdkim_hashes[i].dkim_hashname, len) == 0) + return i; +return -1; +} + +void +pdkim_cstring_to_canons(const uschar * s, unsigned len, + int * canon_head, int * canon_body) +{ +if (!len) len = Ustrlen(s); +for (int i = 0; pdkim_combined_canons[i].str; i++) + if ( Ustrncmp(s, pdkim_combined_canons[i].str, len) == 0 + && len == Ustrlen(pdkim_combined_canons[i].str)) + { + *canon_head = pdkim_combined_canons[i].canon_headers; + *canon_body = pdkim_combined_canons[i].canon_body; + break; + } +} + + + +const char * +pdkim_verify_status_str(int status) +{ +switch(status) + { + case PDKIM_VERIFY_NONE: return "PDKIM_VERIFY_NONE"; + case PDKIM_VERIFY_INVALID: return "PDKIM_VERIFY_INVALID"; + case PDKIM_VERIFY_FAIL: return "PDKIM_VERIFY_FAIL"; + case PDKIM_VERIFY_PASS: return "PDKIM_VERIFY_PASS"; + default: return "PDKIM_VERIFY_UNKNOWN"; + } +} + +const char * +pdkim_verify_ext_status_str(int ext_status) +{ +switch(ext_status) + { + case PDKIM_VERIFY_FAIL_BODY: return "PDKIM_VERIFY_FAIL_BODY"; + case PDKIM_VERIFY_FAIL_MESSAGE: return "PDKIM_VERIFY_FAIL_MESSAGE"; + case PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH: return "PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH"; + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: return "PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE"; + case PDKIM_VERIFY_INVALID_BUFFER_SIZE: return "PDKIM_VERIFY_INVALID_BUFFER_SIZE"; + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: return "PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD"; + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return "PDKIM_VERIFY_INVALID_PUBKEY_IMPORT"; + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: return "PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE"; + case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: return "PDKIM_VERIFY_INVALID_SIGNATURE_ERROR"; + case PDKIM_VERIFY_INVALID_DKIM_VERSION: return "PDKIM_VERIFY_INVALID_DKIM_VERSION"; + default: return "PDKIM_VERIFY_UNKNOWN"; + } +} + +const uschar * +pdkim_errstr(int status) +{ +switch(status) + { + case PDKIM_OK: return US"OK"; + case PDKIM_FAIL: return US"FAIL"; + case PDKIM_ERR_RSA_PRIVKEY: return US"PRIVKEY"; + case PDKIM_ERR_RSA_SIGNING: return US"SIGNING"; + case PDKIM_ERR_LONG_LINE: return US"LONG_LINE"; + case PDKIM_ERR_BUFFER_TOO_SMALL: return US"BUFFER_TOO_SMALL"; + case PDKIM_ERR_EXCESS_SIGS: return US"EXCESS_SIGS"; + case PDKIM_SIGN_PRIVKEY_WRAP: return US"PRIVKEY_WRAP"; + case PDKIM_SIGN_PRIVKEY_B64D: return US"PRIVKEY_B64D"; + default: return US"(unknown)"; + } +} + + +/* -------------------------------------------------------------------------- */ + + +static pdkim_stringlist * +pdkim_prepend_stringlist(pdkim_stringlist * base, const uschar * str) +{ +pdkim_stringlist * new_entry = store_get(sizeof(pdkim_stringlist), GET_UNTAINTED); + +memset(new_entry, 0, sizeof(pdkim_stringlist)); +new_entry->value = string_copy(str); +if (base) new_entry->next = base; +return new_entry; +} + + + +/* Trim whitespace fore & aft */ + +static void +pdkim_strtrim(gstring * str) +{ +uschar * p = str->s; +uschar * q; + +while (*p == '\t' || *p == ' ') /* dump the leading whitespace */ + { str->size--; str->ptr--; str->s++; } + +while ( str->ptr > 0 + && ((q = str->s + str->ptr - 1), (*q == '\t' || *q == ' ')) + ) + str->ptr--; /* dump trailing whitespace */ + +(void) string_from_gstring(str); +} + + + +/* -------------------------------------------------------------------------- */ + +DLLEXPORT void +pdkim_free_ctx(pdkim_ctx *ctx) +{ +} + + +/* -------------------------------------------------------------------------- */ +/* Matches the name of the passed raw "header" against + the passed colon-separated "tick", and invalidates + the entry in tick. Entries can be prefixed for multi- or over-signing, + in which case do not invalidate. + + Returns OK for a match, or fail-code +*/ + +static int +header_name_match(const uschar * header, uschar * tick) +{ +const uschar * ticklist = tick; +int sep = ':'; +BOOL multisign; +uschar * hname, * p, * ele; +uschar * hcolon = Ustrchr(header, ':'); /* Get header name */ + +if (!hcolon) + return PDKIM_FAIL; /* This isn't a header */ + +/* if we had strncmpic() we wouldn't need this copy */ +hname = string_copyn(header, hcolon-header); + +while (p = US ticklist, ele = string_nextinlist(&ticklist, &sep, NULL, 0)) + { + switch (*ele) + { + case '=': case '+': multisign = TRUE; ele++; break; + default: multisign = FALSE; break; + } + + if (strcmpic(ele, hname) == 0) + { + if (!multisign) + *p = '_'; /* Invalidate this header name instance in tick-off list */ + return PDKIM_OK; + } + } +return PDKIM_FAIL; +} + + +/* -------------------------------------------------------------------------- */ +/* Module API */ +/* Performs "relaxed" canonicalization of a header. */ + +uschar * +pdkim_relax_header_n(const uschar * header, int len, BOOL append_crlf) +{ +BOOL past_field_name = FALSE; +BOOL seen_wsp = FALSE; +uschar * relaxed = store_get(len+3, GET_TAINTED); +uschar * q = relaxed; + +for (const uschar * p = header; p - header < len; p++) + { + uschar c = *p; + + if (c == '\r' || c == '\n') /* Ignore CR & LF */ + continue; + if (c == '\t' || c == ' ') + { + if (seen_wsp) + continue; + c = ' '; /* Turns WSP into SP */ + seen_wsp = TRUE; + } + else + if (!past_field_name && c == ':') + { + if (seen_wsp) q--; /* This removes WSP immediately before the colon */ + seen_wsp = TRUE; /* This removes WSP immediately after the colon */ + past_field_name = TRUE; + } + else + seen_wsp = FALSE; + + /* Lowercase header name */ + if (!past_field_name) c = tolower(c); + *q++ = c; + } + +if (q > relaxed && q[-1] == ' ') q--; /* Squash eventual trailing SP */ + +if (append_crlf) { *q++ = '\r'; *q++ = '\n'; } +*q = '\0'; +return relaxed; +} + + +uschar * +pdkim_relax_header(const uschar * header, BOOL append_crlf) +{ +return pdkim_relax_header_n(header, Ustrlen(header), append_crlf); +} + + +/* -------------------------------------------------------------------------- */ +#define PDKIM_QP_ERROR_DECODE -1 + +static const uschar * +pdkim_decode_qp_char(const uschar *qp_p, int *c) +{ +const uschar *initial_pos = qp_p; + +/* Advance one char */ +qp_p++; + +/* Check for two hex digits and decode them */ +if (isxdigit(*qp_p) && isxdigit(qp_p[1])) + { + /* Do hex conversion */ + *c = (isdigit(*qp_p) ? *qp_p - '0' : toupper(*qp_p) - 'A' + 10) << 4; + *c |= isdigit(qp_p[1]) ? qp_p[1] - '0' : toupper(qp_p[1]) - 'A' + 10; + return qp_p + 2; + } + +/* Illegal char here */ +*c = PDKIM_QP_ERROR_DECODE; +return initial_pos; +} + + +/* -------------------------------------------------------------------------- */ + +static uschar * +pdkim_decode_qp(const uschar * str) +{ +int nchar = 0; +uschar * q; +const uschar * p = str; +uschar * n = store_get(Ustrlen(str)+1, GET_TAINTED); + +*n = '\0'; +q = n; +while (*p) + { + if (*p == '=') + { + p = pdkim_decode_qp_char(p, &nchar); + if (nchar >= 0) + { + *q++ = nchar; + continue; + } + } + else + *q++ = *p; + p++; + } +*q = '\0'; +return n; +} + + +/* -------------------------------------------------------------------------- */ + +void +pdkim_decode_base64(const uschar * str, blob * b) +{ +int dlen = b64decode(str, &b->data, str); +if (dlen < 0) b->data = NULL; +b->len = dlen; +} + +uschar * +pdkim_encode_base64(blob * b) +{ +return b64encode(b->data, b->len); +} + + +/* -------------------------------------------------------------------------- */ +#define PDKIM_HDR_LIMBO 0 +#define PDKIM_HDR_TAG 1 +#define PDKIM_HDR_VALUE 2 + +static pdkim_signature * +pdkim_parse_sig_header(pdkim_ctx * ctx, uschar * raw_hdr) +{ +pdkim_signature * sig = store_get(sizeof(pdkim_signature), GET_UNTAINTED); +uschar * q; +gstring * cur_tag = NULL, * cur_val = NULL; +BOOL past_hname = FALSE, in_b_val = FALSE; +int where = PDKIM_HDR_LIMBO; + +memset(sig, 0, sizeof(pdkim_signature)); +sig->bodylength = -1; + +/* Set so invalid/missing data error display is accurate */ +sig->version = 0; +sig->keytype = -1; +sig->hashtype = -1; + +q = sig->rawsig_no_b_val = store_get(Ustrlen(raw_hdr)+1, GET_TAINTED); + +for (uschar * p = raw_hdr; ; p++) + { + char c = *p; + + /* Ignore FWS */ + if (c == '\r' || c == '\n') + goto NEXT_CHAR; + + /* Fast-forward through header name */ + if (!past_hname) + { + if (c == ':') past_hname = TRUE; + goto NEXT_CHAR; + } + + if (where == PDKIM_HDR_LIMBO) + { + /* In limbo, just wait for a tag-char to appear */ + if (!(c >= 'a' && c <= 'z')) + goto NEXT_CHAR; + + where = PDKIM_HDR_TAG; + } + + if (where == PDKIM_HDR_TAG) + if (c == '=') + { + if (Ustrcmp(string_from_gstring(cur_tag), "b") == 0) + { + *q++ = '='; + in_b_val = TRUE; + } + where = PDKIM_HDR_VALUE; + goto NEXT_CHAR; + } + else if (!isspace(c)) + cur_tag = string_catn(cur_tag, p, 1); + + if (where == PDKIM_HDR_VALUE) + { + if (c == '\r' || c == '\n' || c == ' ' || c == '\t') + goto NEXT_CHAR; + + if (c == ';' || c == '\0') + { + /* We must have both tag and value, and tags must be one char except + for the possibility of "bh". */ + + if ( cur_tag && cur_val + && (cur_tag->ptr == 1 || *cur_tag->s == 'b') + ) + { + (void) string_from_gstring(cur_val); + pdkim_strtrim(cur_val); + + DEBUG(D_acl) debug_printf(" %s=%s\n", cur_tag->s, cur_val->s); + + switch (*cur_tag->s) + { + case 'b': /* sig-data or body-hash */ + switch (cur_tag->s[1]) + { + case '\0': pdkim_decode_base64(cur_val->s, &sig->sighash); break; + case 'h': if (cur_tag->ptr != 2) goto bad_tag; + pdkim_decode_base64(cur_val->s, &sig->bodyhash); + break; + default: goto bad_tag; + } + break; + case 'v': /* version */ + /* We only support version 1, and that is currently the + only version there is. */ + sig->version = + Ustrcmp(cur_val->s, PDKIM_SIGNATURE_VERSION) == 0 ? 1 : -1; + break; + case 'a': /* algorithm */ + { + const uschar * list = cur_val->s; + int sep = '-'; + uschar * elem; + + if ((elem = string_nextinlist(&list, &sep, NULL, 0))) + sig->keytype = pdkim_keyname_to_keytype(elem); + if ((elem = string_nextinlist(&list, &sep, NULL, 0))) + for (int i = 0; i < nelem(pdkim_hashes); i++) + if (Ustrcmp(elem, pdkim_hashes[i].dkim_hashname) == 0) + { sig->hashtype = i; break; } + } + break; + + case 'c': /* canonicalization */ + pdkim_cstring_to_canons(cur_val->s, 0, + &sig->canon_headers, &sig->canon_body); + break; + case 'q': /* Query method (for pubkey)*/ + for (int i = 0; pdkim_querymethods[i]; i++) + if (Ustrcmp(cur_val->s, pdkim_querymethods[i]) == 0) + { + sig->querymethod = i; /* we never actually use this */ + break; + } + break; + case 's': /* Selector */ + sig->selector = string_copy_from_gstring(cur_val); break; + case 'd': /* SDID */ + sig->domain = string_copy_from_gstring(cur_val); break; + case 'i': /* AUID */ + sig->identity = pdkim_decode_qp(cur_val->s); break; + case 't': /* Timestamp */ + sig->created = strtoul(CS cur_val->s, NULL, 10); break; + case 'x': /* Expiration */ + sig->expires = strtoul(CS cur_val->s, NULL, 10); break; + case 'l': /* Body length count */ + sig->bodylength = strtol(CS cur_val->s, NULL, 10); break; + case 'h': /* signed header fields */ + sig->headernames = string_copy_from_gstring(cur_val); break; + case 'z': /* Copied headfields */ + sig->copiedheaders = pdkim_decode_qp(cur_val->s); break; +/*XXX draft-ietf-dcrup-dkim-crypto-05 would need 'p' tag support +for rsafp signatures. But later discussion is dropping those. */ + default: + goto bad_tag; + } + } + else +bad_tag: DEBUG(D_acl) debug_printf(" Unknown tag encountered: %Y\n", cur_tag); + + cur_tag = cur_val = NULL; + in_b_val = FALSE; + where = PDKIM_HDR_LIMBO; + } + else + cur_val = string_catn(cur_val, p, 1); + } + +NEXT_CHAR: + if (c == '\0') + break; + + if (!in_b_val) + *q++ = c; + } + +if (sig->keytype < 0 || sig->hashtype < 0) /* Cannot verify this signature */ + return NULL; + +*q = '\0'; +/* Chomp raw header. The final newline must not be added to the signature. */ +while (--q > sig->rawsig_no_b_val && (*q == '\r' || *q == '\n')) + *q = '\0'; + +DEBUG(D_acl) + { + debug_printf( + "DKIM >> Raw signature w/o b= tag value >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + debug_printf("%Z\n", US sig->rawsig_no_b_val); + debug_printf( + "DKIM >> Sig size: %4u bits\n", (unsigned) sig->sighash.len*8); + debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } + +if (!pdkim_set_sig_bodyhash(ctx, sig)) + return NULL; + +return sig; +} + + +/* -------------------------------------------------------------------------- */ + +pdkim_pubkey * +pdkim_parse_pubkey_record(const uschar * raw_record) +{ +pdkim_pubkey * pub = store_get(sizeof(pdkim_pubkey), GET_TAINTED); + +memset(pub, 0, sizeof(pdkim_pubkey)); + +for (const uschar * ele = raw_record, * tspec, * end, * val; *ele; ele = end) + { + Uskip_whitespace(&ele); + end = Ustrchrnul(ele, ';'); + tspec = string_copyn(ele, end - ele); + if (*end) end++; /* skip the ; */ + + if ((val = Ustrchr(tspec, '='))) + { + int taglen = val++ - tspec; + + DEBUG(D_acl) debug_printf(" %.*s=%s\n", taglen, tspec, val); + while (taglen > 1 && isspace(tspec[taglen-1])) + taglen--; /* Ignore whitespace before = */ + Uskip_whitespace(&val); /* Ignore whitespace after = */ + if (isspace(val[ Ustrlen(val)-1 ])) + { /* Ignore whitespace after value */ + gstring * g = string_cat(NULL, val); + while (isspace(gstring_last_char(g))) + gstring_trim(g, 1); + val = string_from_gstring(g); + } + + if (taglen == 1) switch (tspec[0]) + { + case 'v': pub->version = val; break; + case 'h': pub->hashes = val; break; + case 'k': pub->keytype = val; break; + case 'g': pub->granularity = val; break; + case 'n': pub->notes = pdkim_decode_qp(val); break; + case 'p': pdkim_decode_base64(val, &pub->key); break; + case 's': pub->srvtype = val; break; + case 't': if (Ustrchr(val, 'y')) pub->testing = 1; + if (Ustrchr(val, 's')) pub->no_subdomaining = 1; + break; + default: goto bad_tag; + } + else +bad_tag: + DEBUG(D_acl) debug_printf(" Unknown tag encountered\n"); + } + } + +/* Set fallback defaults */ +if (!pub->version) + pub->version = string_copy(PDKIM_PUB_RECORD_VERSION); +else if (Ustrcmp(pub->version, PDKIM_PUB_RECORD_VERSION) != 0) + { + DEBUG(D_acl) debug_printf(" Bad v= field\n"); + return NULL; + } + +if (!pub->granularity) pub->granularity = US"*"; +if (!pub->keytype ) pub->keytype = US"rsa"; +if (!pub->srvtype ) pub->srvtype = US"*"; + +/* p= is required */ +if (pub->key.data) + return pub; + +DEBUG(D_acl) debug_printf(" Missing p= field\n"); +return NULL; +} + + +/* -------------------------------------------------------------------------- */ + +/* Update one bodyhash with some additional data. +If we have to relax the data for this sig, return our copy of it. */ + +static blob * +pdkim_update_ctx_bodyhash(pdkim_bodyhash * b, const blob * orig_data, blob * relaxed_data) +{ +const blob * canon_data = orig_data; +size_t left; + +/* Defaults to simple canon (no further treatment necessary) */ + +if (b->canon_method == PDKIM_CANON_RELAXED) + { + /* Relax the line if not done already */ + if (!relaxed_data) + { + BOOL seen_wsp = FALSE; + int q = 0; + + /* We want to be able to free this else we allocate + for the entire message which could be many MB. Since + we don't know what allocations the SHA routines might + do, not safe to use store_get()/store_reset(). */ + + relaxed_data = store_malloc(sizeof(blob) + orig_data->len+1); + relaxed_data->data = US (relaxed_data+1); + + for (const uschar * p = orig_data->data, * r = p + orig_data->len; p < r; p++) + { + char c = *p; + if (c == '\r') + { + if (q > 0 && relaxed_data->data[q-1] == ' ') + q--; + } + else if (c == '\t' || c == ' ') + { + c = ' '; /* Turns WSP into SP */ + if (seen_wsp) + continue; + seen_wsp = TRUE; + } + else + seen_wsp = FALSE; + relaxed_data->data[q++] = c; + } + relaxed_data->data[q] = '\0'; + relaxed_data->len = q; + } + canon_data = relaxed_data; + } + +/* Make sure we don't exceed the to-be-signed body length */ +left = canon_data->len; +if ( b->bodylength >= 0 + && left > (unsigned long)b->bodylength - b->signed_body_bytes + ) + left = (unsigned long)b->bodylength - b->signed_body_bytes; + +if (left > 0) + { + exim_sha_update(&b->body_hash_ctx, CUS canon_data->data, left); + b->signed_body_bytes += left; + DEBUG(D_acl) debug_printf("%.*Z\n", left, canon_data->data); + } + +return relaxed_data; +} + + +/* -------------------------------------------------------------------------- */ + +static void +pdkim_finish_bodyhash(pdkim_ctx * ctx) +{ +for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) /* Finish hashes */ + { + DEBUG(D_acl) debug_printf("DKIM: finish bodyhash %s/%s/%ld len %ld\n", + pdkim_hashes[b->hashtype].dkim_hashname, pdkim_canons[b->canon_method], + b->bodylength, b->signed_body_bytes); + exim_sha_finish(&b->body_hash_ctx, &b->bh); + } + +/* Traverse all signatures */ +for (pdkim_signature * sig = ctx->sig; sig; sig = sig->next) + { + pdkim_bodyhash * b = sig->calc_body_hash; + + DEBUG(D_acl) + { + debug_printf("DKIM [%s]%s Body bytes (%s) hashed: %lu\n" + "DKIM [%s]%s Body %s computed: ", + sig->domain, sig->selector, pdkim_canons[b->canon_method], b->signed_body_bytes, + sig->domain, sig->selector, pdkim_hashes[b->hashtype].dkim_hashname); + debug_printf("%.*H\n", b->bh.len, CUS b->bh.data); + } + + /* SIGNING -------------------------------------------------------------- */ + if (ctx->flags & PDKIM_MODE_SIGN) + { + /* If bodylength limit is set, and we have received less bytes + than the requested amount, effectively remove the limit tag. */ + if (b->signed_body_bytes < sig->bodylength) + sig->bodylength = -1; + } + + else + /* VERIFICATION --------------------------------------------------------- */ + /* Be careful that the header sig included a bodyash */ + + if (sig->bodyhash.data && sig->bodyhash.len == b->bh.len + && memcmp(b->bh.data, sig->bodyhash.data, b->bh.len) == 0) + { + DEBUG(D_acl) debug_printf("DKIM [%s] Body hash compared OK\n", sig->domain); + } + else + { + DEBUG(D_acl) + { + debug_printf("DKIM [%s] Body hash signature from headers: ", sig->domain); + debug_printf("%.*H\n", sig->bodyhash.len, sig->bodyhash.data); + debug_printf("DKIM [%s] Body hash did NOT verify\n", sig->domain); + } + sig->verify_status = PDKIM_VERIFY_FAIL; + sig->verify_ext_status = PDKIM_VERIFY_FAIL_BODY; + } + } +} + + + +static void +pdkim_body_complete(pdkim_ctx * ctx) +{ +/* In simple body mode, if any empty lines were buffered, +replace with one. rfc 4871 3.4.3 */ +/*XXX checking the signed-body-bytes is a gross hack; I think +it indicates that all linebreaks should be buffered, including +the one terminating a text line */ + +for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) + if ( b->canon_method == PDKIM_CANON_SIMPLE + && b->signed_body_bytes == 0 + && b->num_buffered_blanklines > 0 + ) + (void) pdkim_update_ctx_bodyhash(b, &lineending, NULL); + +ctx->flags |= PDKIM_SEEN_EOD; +ctx->linebuf_offset = 0; +} + + + +/* -------------------------------------------------------------------------- */ +/* Call from pdkim_feed below for processing complete body lines */ +/* NOTE: the line is not NUL-terminated; but we have a count */ + +static void +pdkim_bodyline_complete(pdkim_ctx * ctx) +{ +blob line = {.data = ctx->linebuf, .len = ctx->linebuf_offset}; +blob * rnl = NULL; +blob * rline = NULL; + +/* Ignore extra data if we've seen the end-of-data marker */ +if (ctx->flags & PDKIM_SEEN_EOD) goto all_skip; + +/* We've always got one extra byte to stuff a zero ... */ +ctx->linebuf[line.len] = '\0'; + +/* Terminate on EOD marker */ +if (ctx->flags & PDKIM_DOT_TERM) + { + if (memcmp(line.data, ".\r\n", 3) == 0) + { pdkim_body_complete(ctx); return; } + + /* Unstuff dots */ + if (memcmp(line.data, "..", 2) == 0) + { line.data++; line.len--; } + } + +/* Empty lines need to be buffered until we find a non-empty line */ +if (memcmp(line.data, "\r\n", 2) == 0) + { + for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) + b->num_buffered_blanklines++; + goto all_skip; + } + +/* Process line for each bodyhash separately */ +for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) + { + if (b->canon_method == PDKIM_CANON_RELAXED) + { + /* Lines with just spaces need to be buffered too */ + uschar * cp = line.data; + char c; + + while ((c = *cp)) + { + if (c == '\r' && cp[1] == '\n') break; + if (c != ' ' && c != '\t') goto hash_process; + cp++; + } + + b->num_buffered_blanklines++; + goto hash_skip; + } + +hash_process: + /* At this point, we have a non-empty line, so release the buffered ones. */ + + while (b->num_buffered_blanklines) + { + rnl = pdkim_update_ctx_bodyhash(b, &lineending, rnl); + b->num_buffered_blanklines--; + } + + rline = pdkim_update_ctx_bodyhash(b, &line, rline); +hash_skip: ; + } + +if (rnl) store_free(rnl); +if (rline) store_free(rline); + +all_skip: + +ctx->linebuf_offset = 0; +return; +} + + +/* -------------------------------------------------------------------------- */ +/* Callback from pdkim_feed below for processing complete headers */ +#define DKIM_SIGNATURE_HEADERNAME "DKIM-Signature:" + +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); + +#ifdef EXPERIMENTAL_ARC +/* Feed the header line to ARC processing */ +(void) arc_header_feed(ctx->cur_header, !(ctx->flags & PDKIM_MODE_SIGN)); +#endif + +if (++ctx->num_headers > PDKIM_MAX_HEADERS) goto BAIL; + +/* SIGNING -------------------------------------------------------------- */ +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); + +/* VERIFICATION ----------------------------------------------------------- */ +/* DKIM-Signature: headers are added to the verification list */ +else + { +#ifdef notdef + DEBUG(D_acl) debug_printf("DKIM >> raw hdr: %.*Z\n", + ctx->cur_head->ptr, CUS ctx->cur_header->s); +#endif + if (strncasecmp(CCS ctx->cur_header->s, + DKIM_SIGNATURE_HEADERNAME, + Ustrlen(DKIM_SIGNATURE_HEADERNAME)) == 0) + { + pdkim_signature * sig, * last_sig; + /* Create and chain new signature block. We could error-check for all + required tags here, but prefer to create the internal sig and expicitly + fail verification of it later. */ + + DEBUG(D_acl) debug_printf( + "DKIM >> Found sig, trying to parse >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + + sig = pdkim_parse_sig_header(ctx, ctx->cur_header->s); + + if (!(last_sig = ctx->sig)) + ctx->sig = sig; + else + { + while (last_sig->next) last_sig = last_sig->next; + last_sig->next = sig; + } + + 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'; + return PDKIM_ERR_EXCESS_SIGS; + } + } + + /* all headers are stored for signature verification */ + ctx->headers = pdkim_prepend_stringlist(ctx->headers, ctx->cur_header->s); + } + +BAIL: +ctx->cur_header->s[ctx->cur_header->ptr = 0] = '\0'; /* leave buffer for reuse */ +return PDKIM_OK; +} + + + +/* -------------------------------------------------------------------------- */ +#define HEADER_BUFFER_FRAG_SIZE 256 + +DLLEXPORT int +pdkim_feed(pdkim_ctx * ctx, const uschar * data, unsigned len) +{ +/* Alternate EOD signal, used in non-dotstuffing mode */ +if (!data) + pdkim_body_complete(ctx); + +else for (unsigned p = 0; p < len; p++) + { + uschar c = data[p]; + int rc; + + if (ctx->flags & PDKIM_PAST_HDRS) + { + if (c == '\n' && !(ctx->flags & PDKIM_SEEN_CR)) /* emulate the CR */ + { + ctx->linebuf[ctx->linebuf_offset++] = '\r'; + if (ctx->linebuf_offset == PDKIM_MAX_BODY_LINE_LEN-1) + return PDKIM_ERR_LONG_LINE; + } + + /* Processing body byte */ + ctx->linebuf[ctx->linebuf_offset++] = c; + if (c == '\r') + ctx->flags |= PDKIM_SEEN_CR; + else if (c == '\n') + { + ctx->flags &= ~PDKIM_SEEN_CR; + pdkim_bodyline_complete(ctx); + } + + if (ctx->linebuf_offset == PDKIM_MAX_BODY_LINE_LEN-1) + return PDKIM_ERR_LONG_LINE; + } + else + { + /* Processing header byte */ + if (c == '\r') + ctx->flags |= PDKIM_SEEN_CR; + else if (c == '\n') + { + if (!(ctx->flags & PDKIM_SEEN_CR)) /* emulate the CR */ + ctx->cur_header = string_catn(ctx->cur_header, CUS "\r", 1); + + if (ctx->flags & PDKIM_SEEN_LF) /* Seen last header line */ + { + if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK) + return rc; + + ctx->flags = (ctx->flags & ~(PDKIM_SEEN_LF|PDKIM_SEEN_CR)) | PDKIM_PAST_HDRS; + DEBUG(D_acl) debug_printf( + "DKIM >> Body data for hash, canonicalized >>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + continue; + } + else + ctx->flags = (ctx->flags & ~PDKIM_SEEN_CR) | PDKIM_SEEN_LF; + } + else if (ctx->flags & PDKIM_SEEN_LF) + { + if (!(c == '\t' || c == ' ')) /* End of header */ + if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK) + return rc; + ctx->flags &= ~PDKIM_SEEN_LF; + } + + if (!ctx->cur_header || ctx->cur_header->ptr < PDKIM_MAX_HEADER_LEN) + ctx->cur_header = string_catn(ctx->cur_header, CUS &data[p], 1); + } + } +return PDKIM_OK; +} + + + +/* Extend a growing header with a continuation-linebreak */ +static gstring * +pdkim_hdr_cont(gstring * str, int * col) +{ +*col = 1; +return string_catn(str, US"\r\n\t", 3); +} + + + +/* + * RFC 5322 specifies that header line length SHOULD be no more than 78 + * pdkim_headcat + * + * Returns gstring (not nul-terminated) appending to one supplied + * + * col: this int holds and receives column number (octets since last '\n') + * str: partial string to append to + * pad: padding, split line or space after before or after eg: ";". + * Only the initial charater is used. + * intro: - must join to payload eg "h=", usually the tag name + * payload: eg base64 data - long data can be split arbitrarily. + * + * this code doesn't fold the header in some of the places that RFC4871 + * allows: As per RFC5322(2.2.3) it only folds before or after tag-value + * pairs and inside long values. it also always spaces or breaks after the + * "pad" + * + * No guarantees are made for output given out-of range input. like tag + * names longer than 78, or bogus col. Input is assumed to be free of line breaks. + */ + +static gstring * +pdkim_headcat(int * col, gstring * str, + const uschar * pad, const uschar * intro, const uschar * payload) +{ +int len, chomp, padded = 0; + +/* If we can fit at least the pad at the end of current line, do it now. +Otherwise, wrap if there is a pad. */ + +if (pad) + if (*col + 1 <= 78) + { + str = string_catn(str, pad, 1); + (*col)++; + pad = NULL; + padded = 1; + } + else + str = pdkim_hdr_cont(str, col); + +/* Special case: if the whole addition does not fit at the end of the current +line, but could fit on a new line, wrap to give it its full, dedicated line. */ + +len = (pad ? 2 : padded) + + (intro ? Ustrlen(intro) : 0) + + (payload ? Ustrlen(payload) : 0); +if (len <= 77 && *col+len > 78) + { + str = pdkim_hdr_cont(str, col); + padded = 0; + } + +/* Either we already dealt with the pad or we know there is room */ + +if (pad) + { + str = string_catn(str, pad, 1); + str = string_catn(str, US" ", 1); + *col += 2; + } +else if (padded && *col < 78) + { + str = string_catn(str, US" ", 1); + (*col)++; + } + +/* Call recursively with intro as payload: it gets the same, special treatment +(that is, not split if < 78). */ + +if (intro) + str = pdkim_headcat(col, str, NULL, NULL, intro); + +if (payload) + for (len = Ustrlen(payload); len; len -= chomp) + { + if (*col >= 78) + str = pdkim_hdr_cont(str, col); + chomp = *col+len > 78 ? 78 - *col : len; + str = string_catn(str, payload, chomp); + *col += chomp; + payload += chomp; + } + +return str; +} + + +/* -------------------------------------------------------------------------- */ + +/* Signing: create signature header +*/ +static uschar * +pdkim_create_header(pdkim_signature * sig, BOOL final) +{ +uschar * base64_bh; +uschar * base64_b; +int col = 0; +gstring * hdr; +gstring * canon_all; + +canon_all = string_cat (NULL, pdkim_canons[sig->canon_headers]); +canon_all = string_catn(canon_all, US"/", 1); +canon_all = string_cat (canon_all, pdkim_canons[sig->canon_body]); +(void) string_from_gstring(canon_all); + +hdr = string_cat(NULL, US"DKIM-Signature: v="PDKIM_SIGNATURE_VERSION); +col = hdr->ptr; + +/* Required and static bits */ +hdr = pdkim_headcat(&col, hdr, US";", US"a=", dkim_sig_to_a_tag(sig)); +hdr = pdkim_headcat(&col, hdr, US";", US"q=", pdkim_querymethods[sig->querymethod]); +hdr = pdkim_headcat(&col, hdr, US";", US"c=", canon_all->s); +hdr = pdkim_headcat(&col, hdr, US";", US"d=", sig->domain); +hdr = pdkim_headcat(&col, hdr, US";", US"s=", sig->selector); + +/* list of header names can be split between items. */ + { + uschar * n = string_copy(sig->headernames); + uschar * i = US"h="; + uschar * s = US";"; + + while (*n) + { + uschar * c = Ustrchr(n, ':'); + + if (c) *c ='\0'; + + if (!i) + hdr = pdkim_headcat(&col, hdr, NULL, NULL, US":"); + + hdr = pdkim_headcat(&col, hdr, s, i, n); + + if (!c) + break; + + n = c+1; + s = NULL; + i = NULL; + } + } + +base64_bh = pdkim_encode_base64(&sig->calc_body_hash->bh); +hdr = pdkim_headcat(&col, hdr, US";", US"bh=", base64_bh); + +/* Optional bits */ +if (sig->identity) + hdr = pdkim_headcat(&col, hdr, US";", US"i=", sig->identity); + +if (sig->created > 0) + { + uschar minibuf[21]; + + snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->created); + hdr = pdkim_headcat(&col, hdr, US";", US"t=", minibuf); +} + +if (sig->expires > 0) + { + uschar minibuf[21]; + + snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->expires); + hdr = pdkim_headcat(&col, hdr, US";", US"x=", minibuf); + } + +if (sig->bodylength >= 0) + { + uschar minibuf[21]; + + snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->bodylength); + hdr = pdkim_headcat(&col, hdr, US";", US"l=", minibuf); + } + +/* Preliminary or final version? */ +if (final) + { + base64_b = pdkim_encode_base64(&sig->sighash); + hdr = pdkim_headcat(&col, hdr, US";", US"b=", base64_b); + + /* add trailing semicolon: I'm not sure if this is actually needed */ + hdr = pdkim_headcat(&col, hdr, NULL, US";", US""); + } +else + { + /* To satisfy the rule "all surrounding whitespace [...] deleted" + ( RFC 6376 section 3.7 ) we ensure there is no whitespace here. Otherwise + the headcat routine could insert a linebreak which the relaxer would reduce + to a single space preceding the terminating semicolon, resulting in an + incorrect header-hash. */ + hdr = pdkim_headcat(&col, hdr, US";", US"b=;", US""); + } + +return string_from_gstring(hdr); +} + + +/* -------------------------------------------------------------------------- */ + +/* According to draft-ietf-dcrup-dkim-crypto-07 "keys are 256 bits" (referring +to DNS, hence the pubkey). Check for more than 32 bytes; if so assume the +alternate possible representation (still) being discussed: a +SubjectPublickeyInfo wrapped key - and drop all but the trailing 32-bytes (it +should be a DER, with exactly 12 leading bytes - but we could accept a BER also, +which could be any size). We still rely on the crypto library for checking for +undersize. + +When the RFC is published this should be re-addressed. */ + +static void +check_bare_ed25519_pubkey(pdkim_pubkey * p) +{ +int excess = p->key.len - 32; +if (excess > 0) + { + DEBUG(D_acl) + debug_printf("DKIM: unexpected pubkey len %lu\n", (unsigned long) p->key.len); + p->key.data += excess; p->key.len = 32; + } +} + + +static pdkim_pubkey * +pdkim_key_from_dns(pdkim_ctx * ctx, pdkim_signature * sig, ev_ctx * vctx, + const uschar ** errstr) +{ +uschar * dns_txt_name, * dns_txt_reply; +pdkim_pubkey * p; + +/* Fetch public key for signing domain, from DNS */ + +dns_txt_name = string_sprintf("%s._domainkey.%s.", sig->selector, sig->domain); + +if ( !(dns_txt_reply = ctx->dns_txt_callback(dns_txt_name)) + || dns_txt_reply[0] == '\0' + ) + { + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE; + return NULL; + } + +DEBUG(D_acl) + { + debug_printf( + "DKIM >> Parsing public key record >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + " %s\n" + " Raw record: %Z\n", + dns_txt_name, + CUS dns_txt_reply); + } + +if ( !(p = pdkim_parse_pubkey_record(CUS dns_txt_reply)) + || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0) + ) + { + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD; + + DEBUG(D_acl) + { + if (p) + debug_printf(" Invalid public key service type '%s'\n", p->srvtype); + else + debug_printf(" Error while parsing public key record\n"); + debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } + return NULL; + } + +DEBUG(D_acl) debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + +/* Import public key */ + +/* Normally we use the signature a= tag to tell us the pubkey format. +When signing under debug we do a test-import of the pubkey, and at that +time we do not have a signature so we must interpret the pubkey k= tag +instead. Assume writing on the sig is ok in that case. */ + +if (sig->keytype < 0) + if ((sig->keytype = pdkim_keyname_to_keytype(p->keytype)) < 0) + { + DEBUG(D_acl) debug_printf("verify_init: unhandled keytype %s\n", p->keytype); + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_IMPORT; + return NULL; + } + +if (sig->keytype == KEYTYPE_ED25519) + check_bare_ed25519_pubkey(p); + +if ((*errstr = exim_dkim_verify_init(&p->key, + sig->keytype == KEYTYPE_ED25519 ? KEYFMT_ED25519_BARE : KEYFMT_DER, + vctx, &sig->keybits))) + { + DEBUG(D_acl) debug_printf("verify_init: %s\n", *errstr); + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_IMPORT; + return NULL; + } + +vctx->keytype = sig->keytype; +return p; +} + + +/* -------------------------------------------------------------------------- */ +/* Sort and filter the sigs developed from the message */ + +static pdkim_signature * +sort_sig_methods(pdkim_signature * siglist) +{ +pdkim_signature * yield, ** ss; +const uschar * prefs; +uschar * ele; +int sep; + +if (!siglist) return NULL; + +/* first select in order of hashtypes */ +DEBUG(D_acl) debug_printf("DKIM: dkim_verify_hashes '%s'\n", dkim_verify_hashes); +for (prefs = dkim_verify_hashes, sep = 0, yield = NULL, ss = &yield; + ele = string_nextinlist(&prefs, &sep, NULL, 0); ) + { + int i = pdkim_hashname_to_hashtype(CUS ele, 0); + for (pdkim_signature * s = siglist, * next, ** prev = &siglist; s; + s = next) + { + next = s->next; + if (s->hashtype == i) + { *prev = next; s->next = NULL; *ss = s; ss = &s->next; } + else + prev = &s->next; + } + } + +/* then in order of keytypes */ +siglist = yield; +DEBUG(D_acl) debug_printf("DKIM: dkim_verify_keytypes '%s'\n", dkim_verify_keytypes); +for (prefs = dkim_verify_keytypes, sep = 0, yield = NULL, ss = &yield; + ele = string_nextinlist(&prefs, &sep, NULL, 0); ) + { + int i = pdkim_keyname_to_keytype(CUS ele); + for (pdkim_signature * s = siglist, * next, ** prev = &siglist; s; + s = next) + { + next = s->next; + if (s->keytype == i) + { *prev = next; s->next = NULL; *ss = s; ss = &s->next; } + else + prev = &s->next; + } + } + +DEBUG(D_acl) for (pdkim_signature * s = yield; s; s = s->next) + debug_printf(" retain d=%s s=%s a=%s\n", + s->domain, s->selector, dkim_sig_to_a_tag(s)); +return yield; +} + + +/* -------------------------------------------------------------------------- */ + +DLLEXPORT int +pdkim_feed_finish(pdkim_ctx * ctx, pdkim_signature ** return_signatures, + const uschar ** err) +{ +BOOL verify_pass = FALSE; + +/* Check if we must still flush a (partial) header. If that is the + case, the message has no body, and we must compute a body hash + out of '' */ +if (ctx->cur_header && ctx->cur_header->ptr > 0) + { + blob * rnl = NULL; + int rc; + + if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK) + return rc; + + for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) + rnl = pdkim_update_ctx_bodyhash(b, &lineending, rnl); + if (rnl) store_free(rnl); + } +else + DEBUG(D_acl) debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + +/* Build (and/or evaluate) body hash. Do this even if no DKIM sigs, in case we +have a hash to do for ARC. */ + +pdkim_finish_bodyhash(ctx); + +/* Sort and filter the recived signatures */ + +if (!(ctx->flags & PDKIM_MODE_SIGN)) + ctx->sig = sort_sig_methods(ctx->sig); + +if (!ctx->sig) + { + DEBUG(D_acl) debug_printf("DKIM: no signatures\n"); + *return_signatures = NULL; + return PDKIM_OK; + } + +for (pdkim_signature * sig = ctx->sig; sig; sig = sig->next) + { + hctx hhash_ctx; + uschar * sig_hdr = US""; + blob hhash; + gstring * hdata = NULL; + es_ctx sctx; + + if ( !(ctx->flags & PDKIM_MODE_SIGN) + && sig->verify_status == PDKIM_VERIFY_FAIL) + { + DEBUG(D_acl) + debug_printf("DKIM: [%s] abandoning this signature\n", sig->domain); + continue; + } + + /*XXX The hash of the headers is needed for GCrypt (for which we can do RSA + signing only, as it happens) and for either GnuTLS and OpenSSL when we are + signing with EC (specifically, Ed25519). The former is because the GCrypt + signing operation is pure (does not do its own hash) so we must hash. The + latter is because we (stupidly, but this is what the IETF draft is saying) + must hash with the declared hash method, then pass the result to the library + hash-and-sign routine (because that's all the libraries are providing. And + we're stuck with whatever that hidden hash method is, too). We may as well + do this hash incrementally. + We don't need the hash we're calculating here for the GnuTLS and OpenSSL + cases of RSA signing, since those library routines can do hash-and-sign. + + Some time in the future we could easily avoid doing the hash here for those + cases (which will be common for a long while. We could also change from + the current copy-all-the-headers-into-one-block, then call the hash-and-sign + implementation - to a proper incremental one. Unfortunately, GnuTLS just + cannot do incremental - either signing or verification. Unsure about GCrypt. + */ + + /*XXX The header hash is also used (so far) by the verify operation */ + + if (!exim_sha_init(&hhash_ctx, pdkim_hashes[sig->hashtype].exim_hashmethod)) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "DKIM: hash setup error, possibly nonhandled hashtype"); + break; + } + + if (ctx->flags & PDKIM_MODE_SIGN) + DEBUG(D_acl) debug_printf( + "DKIM >> Headers to be signed: >>>>>>>>>>>>\n" + " %s\n", + sig->sign_headers); + + DEBUG(D_acl) debug_printf( + "DKIM >> Header data for hash, canonicalized (%-7s), in sequence >>\n", + pdkim_canons[sig->canon_headers]); + + + /* SIGNING ---------------------------------------------------------------- */ + /* When signing, walk through our header list and add them to the hash. As we + go, construct a list of the header's names to use for the h= parameter. + Then append to that list any remaining header names for which there was no + header to sign. */ + + if (ctx->flags & PDKIM_MODE_SIGN) + { + gstring * g = NULL; + const uschar * l; + uschar * s; + int sep = 0; + + /* Import private key, including the keytype which we need for building + the signature header */ + + if ((*err = exim_dkim_signing_init(CUS sig->privkey, &sctx))) + { + log_write(0, LOG_MAIN|LOG_PANIC, "signing_init: %s", *err); + return PDKIM_ERR_RSA_PRIVKEY; + } + sig->keytype = sctx.keytype; + + sig->headernames = NULL; /* Collected signed header names */ + for (pdkim_stringlist * p = sig->headers; p; p = p->next) + { + uschar * rh = p->value; + + if (header_name_match(rh, sig->sign_headers) == PDKIM_OK) + { + /* Collect header names (Note: colon presence is guaranteed here) */ + g = string_append_listele_n(g, ':', rh, Ustrchr(rh, ':') - rh); + + if (sig->canon_headers == PDKIM_CANON_RELAXED) + rh = pdkim_relax_header(rh, TRUE); /* cook header for relaxed canon */ + + /* Feed header to the hash algorithm */ + exim_sha_update_string(&hhash_ctx, CUS rh); + + /* Remember headers block for signing (when the library cannot do incremental) */ + /*XXX we could avoid doing this for all but the GnuTLS/RSA case */ + hdata = exim_dkim_data_append(hdata, rh); + + DEBUG(D_acl) debug_printf("%Z\n", rh); + } + } + + /* Any headers we wanted to sign but were not present must also be listed. + Ignore elements that have been ticked-off or are marked as never-oversign. */ + + l = sig->sign_headers; + while((s = string_nextinlist(&l, &sep, NULL, 0))) + { + if (*s == '+') /* skip oversigning marker */ + s++; + if (*s != '_' && *s != '=') + g = string_append_listele(g, ':', s); + } + sig->headernames = string_from_gstring(g); + + /* Create signature header with b= omitted */ + sig_hdr = pdkim_create_header(sig, FALSE); + } + + /* VERIFICATION ----------------------------------------------------------- */ + /* When verifying, walk through the header name list in the h= parameter and + add the headers to the hash in that order. */ + else + { + uschar * p = sig->headernames; + uschar * q; + + if (p) + { + /* clear tags */ + for (pdkim_stringlist * hdrs = ctx->headers; hdrs; hdrs = hdrs->next) + hdrs->tag = 0; + + p = string_copy(p); + while(1) + { + if ((q = Ustrchr(p, ':'))) + *q = '\0'; + + /*XXX walk the list of headers in same order as received. */ + for (pdkim_stringlist * hdrs = ctx->headers; hdrs; hdrs = hdrs->next) + if ( hdrs->tag == 0 + && strncasecmp(CCS hdrs->value, CCS p, Ustrlen(p)) == 0 + && (hdrs->value)[Ustrlen(p)] == ':' + ) + { + /* cook header for relaxed canon, or just copy it for simple */ + + uschar * rh = sig->canon_headers == PDKIM_CANON_RELAXED + ? pdkim_relax_header(hdrs->value, TRUE) + : string_copy(CUS hdrs->value); + + /* Feed header to the hash algorithm */ + exim_sha_update_string(&hhash_ctx, CUS rh); + + DEBUG(D_acl) debug_printf("%Z\n", rh); + hdrs->tag = 1; + break; + } + + if (!q) break; + p = q+1; + } + + sig_hdr = string_copy(sig->rawsig_no_b_val); + } + } + + DEBUG(D_acl) debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + + DEBUG(D_acl) + { + debug_printf( + "DKIM >> Signed DKIM-Signature header, pre-canonicalized >>>>>>>>>>>>>\n"); + debug_printf("%Z\n", CUS sig_hdr); + debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } + + /* Relax header if necessary */ + if (sig->canon_headers == PDKIM_CANON_RELAXED) + sig_hdr = pdkim_relax_header(sig_hdr, FALSE); + + DEBUG(D_acl) + { + debug_printf("DKIM >> Signed DKIM-Signature header, canonicalized (%-7s) >>>>>>>\n", + pdkim_canons[sig->canon_headers]); + debug_printf("%Z\n", CUS sig_hdr); + debug_printf( + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } + + /* Finalize header hash */ + exim_sha_update_string(&hhash_ctx, CUS sig_hdr); + exim_sha_finish(&hhash_ctx, &hhash); + + DEBUG(D_acl) + { + debug_printf("DKIM [%s] Header %s computed: ", + sig->domain, pdkim_hashes[sig->hashtype].dkim_hashname); + debug_printf("%.*H\n", hhash.len, hhash.data); + } + + /* Remember headers block for signing (when the signing library cannot do + incremental) */ + if (ctx->flags & PDKIM_MODE_SIGN) + hdata = exim_dkim_data_append(hdata, US sig_hdr); + + /* SIGNING ---------------------------------------------------------------- */ + if (ctx->flags & PDKIM_MODE_SIGN) + { + hashmethod hm = sig->keytype == KEYTYPE_ED25519 +#if defined(SIGN_OPENSSL) + ? HASH_NULL +#else + ? HASH_SHA2_512 +#endif + : pdkim_hashes[sig->hashtype].exim_hashmethod; + +#ifdef SIGN_HAVE_ED25519 + /* For GCrypt, and for EC, we pass the hash-of-headers to the signing + routine. For anything else we just pass the headers. */ + + if (sig->keytype != KEYTYPE_ED25519) +#endif + { + hhash.data = hdata->s; + hhash.len = hdata->ptr; + } + + if ((*err = exim_dkim_sign(&sctx, hm, &hhash, &sig->sighash))) + { + log_write(0, LOG_MAIN|LOG_PANIC, "signing: %s", *err); + return PDKIM_ERR_RSA_SIGNING; + } + + DEBUG(D_acl) + { + debug_printf( "DKIM [%s] b computed: ", sig->domain); + debug_printf("%.*H\n", sig->sighash.len, sig->sighash.data); + } + + sig->signature_header = pdkim_create_header(sig, TRUE); + } + + /* VERIFICATION ----------------------------------------------------------- */ + else + { + ev_ctx vctx; + hashmethod hm; + + /* Make sure we have all required signature tags */ + if (!( sig->domain && *sig->domain + && sig->selector && *sig->selector + && sig->headernames && *sig->headernames + && sig->bodyhash.data + && sig->sighash.data + && sig->keytype >= 0 + && sig->hashtype >= 0 + && sig->version + ) ) + { + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_SIGNATURE_ERROR; + + DEBUG(D_acl) debug_printf( + " Error in DKIM-Signature header: tags missing or invalid (%s)\n" + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n", + !(sig->domain && *sig->domain) ? "d=" + : !(sig->selector && *sig->selector) ? "s=" + : !(sig->headernames && *sig->headernames) ? "h=" + : !sig->bodyhash.data ? "bh=" + : !sig->sighash.data ? "b=" + : sig->keytype < 0 || sig->hashtype < 0 ? "a=" + : "v=" + ); + goto NEXT_VERIFY; + } + + /* Make sure sig uses supported DKIM version (only v1) */ + if (sig->version != 1) + { + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_DKIM_VERSION; + + DEBUG(D_acl) debug_printf( + " Error in DKIM-Signature header: unsupported DKIM version\n" + "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + goto NEXT_VERIFY; + } + + DEBUG(D_acl) + { + debug_printf( "DKIM [%s] b from mail: ", sig->domain); + debug_printf("%.*H\n", sig->sighash.len, sig->sighash.data); + } + + if (!(sig->pubkey = pdkim_key_from_dns(ctx, sig, &vctx, err))) + { + log_write(0, LOG_MAIN, "DKIM: %s%s %s%s [failed key import]", + sig->domain ? "d=" : "", sig->domain ? sig->domain : US"", + sig->selector ? "s=" : "", sig->selector ? sig->selector : US""); + goto NEXT_VERIFY; + } + + /* If the pubkey limits to a list of specific hashes, ignore sigs that + do not have the hash part of the sig algorithm matching */ + + if (sig->pubkey->hashes) + { + const uschar * list = sig->pubkey->hashes, * ele; + int sep = ':'; + while ((ele = string_nextinlist(&list, &sep, NULL, 0))) + if (Ustrcmp(ele, pdkim_hashes[sig->hashtype].dkim_hashname) == 0) break; + if (!ele) + { + DEBUG(D_acl) debug_printf("pubkey h=%s vs. sig a=%s_%s\n", + sig->pubkey->hashes, + pdkim_keytypes[sig->keytype], + pdkim_hashes[sig->hashtype].dkim_hashname); + sig->verify_status = PDKIM_VERIFY_FAIL; + sig->verify_ext_status = PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH; + goto NEXT_VERIFY; + } + } + + hm = sig->keytype == KEYTYPE_ED25519 +#if defined(SIGN_OPENSSL) + ? HASH_NULL +#else + ? HASH_SHA2_512 +#endif + : pdkim_hashes[sig->hashtype].exim_hashmethod; + + /* Check the signature */ + + if ((*err = exim_dkim_verify(&vctx, hm, &hhash, &sig->sighash))) + { + DEBUG(D_acl) debug_printf("headers verify: %s\n", *err); + sig->verify_status = PDKIM_VERIFY_FAIL; + sig->verify_ext_status = PDKIM_VERIFY_FAIL_MESSAGE; + goto NEXT_VERIFY; + } + if (*dkim_verify_min_keysizes) + { + unsigned minbits; + const uschar * ss = expand_getkeyed(US pdkim_keytypes[sig->keytype], + dkim_verify_min_keysizes); + if (ss && (minbits = atoi(CCS ss)) > sig->keybits) + { + DEBUG(D_acl) debug_printf("Key too short: Actual: %s %u Minima '%s'\n", + pdkim_keytypes[sig->keytype], sig->keybits, dkim_verify_min_keysizes); + sig->verify_status = PDKIM_VERIFY_FAIL; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE; + } + } + + + /* We have a winner! (if bodyhash was correct earlier) */ + if (sig->verify_status == PDKIM_VERIFY_NONE) + { + sig->verify_status = PDKIM_VERIFY_PASS; + verify_pass = TRUE; + /*XXX We used to "break" here if dkim_verify_minimal, but that didn't + stop the ACL being called. So move that test. Unfortunately, we + need to eval all the sigs here only to possibly ignore some later, + because we don't know what verify options might say. + Could we change to a later eval of the sig? + Both bits are called from receive_msg(). + Moving the test is also suboptimal for the case of no ACL (or no + signers to check!) so keep it for that case, but after debug output */ + } + +NEXT_VERIFY: + DEBUG(D_acl) + { + debug_printf("DKIM [%s] %s signature status: %s", + sig->domain, dkim_sig_to_a_tag(sig), + pdkim_verify_status_str(sig->verify_status)); + if (sig->verify_ext_status > 0) + debug_printf(" (%s)\n", + pdkim_verify_ext_status_str(sig->verify_ext_status)); + else + debug_printf("\n"); + } + + if ( verify_pass && dkim_verify_minimal + && !(acl_smtp_dkim && dkim_verify_signers && *dkim_verify_signers)) + break; + } + } + +/* If requested, set return pointer to signature(s) */ +if (return_signatures) + *return_signatures = ctx->sig; + +return ctx->flags & PDKIM_MODE_SIGN || verify_pass + ? PDKIM_OK : PDKIM_FAIL; +} + + +/* -------------------------------------------------------------------------- */ + +DLLEXPORT pdkim_ctx * +pdkim_init_verify(uschar * (*dns_txt_callback)(const uschar *), BOOL dot_stuffing) +{ +pdkim_ctx * ctx; + +ctx = store_get(sizeof(pdkim_ctx), GET_UNTAINTED); +memset(ctx, 0, sizeof(pdkim_ctx)); + +if (dot_stuffing) ctx->flags = PDKIM_DOT_TERM; +/* The line-buffer is for message data, hence tainted */ +ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN, GET_TAINTED); +ctx->dns_txt_callback = dns_txt_callback; +ctx->cur_header = string_get_tainted(36, GET_TAINTED); + +return ctx; +} + + +/* -------------------------------------------------------------------------- */ + +DLLEXPORT pdkim_signature * +pdkim_init_sign(pdkim_ctx * ctx, + uschar * domain, uschar * selector, uschar * privkey, + uschar * hashname, const uschar ** errstr) +{ +int hashtype; +pdkim_signature * sig; + +if (!domain || !selector || !privkey) + return NULL; + +/* Allocate & init one signature struct */ + +sig = store_get(sizeof(pdkim_signature), GET_UNTAINTED); +memset(sig, 0, sizeof(pdkim_signature)); + +sig->bodylength = -1; + +sig->domain = string_copy(US domain); +sig->selector = string_copy(US selector); +sig->privkey = string_copy(US privkey); +sig->keytype = -1; + +for (hashtype = 0; hashtype < nelem(pdkim_hashes); hashtype++) + if (Ustrcmp(hashname, pdkim_hashes[hashtype].dkim_hashname) == 0) + { sig->hashtype = hashtype; break; } +if (hashtype >= nelem(pdkim_hashes)) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "DKIM: unrecognised hashname '%s'", hashname); + return NULL; + } + +DEBUG(D_acl) + { + pdkim_signature s = *sig; + ev_ctx vctx; + + debug_printf("DKIM (checking verify key)>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + if (!pdkim_key_from_dns(ctx, &s, &vctx, errstr)) + debug_printf("WARNING: bad dkim key in dns\n"); + debug_printf("DKIM (finished checking verify key)<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } +return sig; +} + + +/* -------------------------------------------------------------------------- */ + +DLLEXPORT void +pdkim_set_optional(pdkim_signature * sig, + char * sign_headers, + char * identity, + int canon_headers, + int canon_body, + long bodylength, + unsigned long created, + unsigned long expires) +{ +if (identity) + sig->identity = string_copy(US identity); + +sig->sign_headers = string_copy(sign_headers + ? US sign_headers : US PDKIM_DEFAULT_SIGN_HEADERS); + +sig->canon_headers = canon_headers; +sig->canon_body = canon_body; +sig->bodylength = bodylength; +sig->created = created; +sig->expires = expires; + +return; +} + + + +/* Set up a blob for calculating the bodyhash according to the +given needs. Use an existing one if possible, or create a new one. + +Return: hashblob pointer, or NULL on error +*/ +pdkim_bodyhash * +pdkim_set_bodyhash(pdkim_ctx * ctx, int hashtype, int canon_method, + long bodylength) +{ +pdkim_bodyhash * b; + +if (hashtype == -1 || canon_method == -1) return NULL; + +if (!ctx) + { + DEBUG(D_receive) debug_printf("pdkim_set_bodyhash: null context\n"); + return NULL; + } + +for (b = ctx->bodyhash; b; b = b->next) + if ( hashtype == b->hashtype + && canon_method == b->canon_method + && bodylength == b->bodylength) + { + DEBUG(D_receive) debug_printf("DKIM: using existing bodyhash %s/%s/%ld\n", + pdkim_hashes[hashtype].dkim_hashname, pdkim_canons[canon_method], bodylength); + return b; + } + +DEBUG(D_receive) debug_printf("DKIM: new bodyhash %s/%s/%ld\n", + pdkim_hashes[hashtype].dkim_hashname, pdkim_canons[canon_method], bodylength); +b = store_get(sizeof(pdkim_bodyhash), GET_UNTAINTED); +b->next = ctx->bodyhash; +b->hashtype = hashtype; +b->canon_method = canon_method; +b->bodylength = bodylength; +if (!exim_sha_init(&b->body_hash_ctx, /*XXX hash method: extend for sha512 */ + pdkim_hashes[hashtype].exim_hashmethod)) + { + DEBUG(D_acl) + debug_printf("DKIM: hash init error, possibly nonhandled hashtype\n"); + return NULL; + } +b->signed_body_bytes = 0; +b->num_buffered_blanklines = 0; +ctx->bodyhash = b; +return b; +} + + +/* Set up a blob for calculating the bodyhash according to the +needs of this signature. Use an existing one if possible, or +create a new one. + +Return: hashblob pointer, or NULL on error (only used as a boolean). +*/ +pdkim_bodyhash * +pdkim_set_sig_bodyhash(pdkim_ctx * ctx, pdkim_signature * sig) +{ +pdkim_bodyhash * b = pdkim_set_bodyhash(ctx, + sig->hashtype, sig->canon_body, sig->bodylength); +sig->calc_body_hash = b; +return b; +} + + +/* -------------------------------------------------------------------------- */ + + +void +pdkim_init_context(pdkim_ctx * ctx, BOOL dot_stuffed, + uschar * (*dns_txt_callback)(const uschar *)) +{ +memset(ctx, 0, sizeof(pdkim_ctx)); +ctx->flags = dot_stuffed ? PDKIM_MODE_SIGN | PDKIM_DOT_TERM : PDKIM_MODE_SIGN; +/* The line buffer is for message data, hence tainted */ +ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN, GET_TAINTED); +DEBUG(D_acl) ctx->dns_txt_callback = dns_txt_callback; +} + + +void +pdkim_init(void) +{ +exim_dkim_signers_init(); +} + + + +#endif /*DISABLE_DKIM*/ diff --git a/src/src/miscmods/pdkim/pdkim.h b/src/src/miscmods/pdkim/pdkim.h new file mode 100644 index 000000000..b86014f8f --- /dev/null +++ b/src/src/miscmods/pdkim/pdkim.h @@ -0,0 +1,368 @@ +/* + * PDKIM - a RFC4871 (DKIM) implementation + * + * Copyright (C) 2009 - 2012 Tom Kistner + * Copyright (c) 2016 - 2020 Jeremy Harris + * SPDX-License-Identifier: GPL-2.0-or-later + * + * http://duncanthrax.net/pdkim/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#ifndef PDKIM_H +#define PDKIM_H + +#include "../blob.h" +#include "../hash.h" + +#define PDKIM_DEFAULT_SIGN_HEADERS "From:Sender:Reply-To:Subject:Date:"\ + "Message-ID:To:Cc:MIME-Version:Content-Type:"\ + "Content-Transfer-Encoding:Content-ID:"\ + "Content-Description:Resent-Date:Resent-From:"\ + "Resent-Sender:Resent-To:Resent-Cc:"\ + "Resent-Message-ID:In-Reply-To:References:"\ + "List-Id:List-Help:List-Unsubscribe:"\ + "List-Subscribe:List-Post:List-Owner:List-Archive" + +#define PDKIM_OVERSIGN_HEADERS "+From:+Sender:+Reply-To:+Subject:+Date:"\ + "+Message-ID:+To:+Cc:+MIME-Version:+Content-Type:"\ + "+Content-Transfer-Encoding:+Content-ID:"\ + "+Content-Description:+Resent-Date:+Resent-From:"\ + "+Resent-Sender:+Resent-To:+Resent-Cc:"\ + "+Resent-Message-ID:+In-Reply-To:+References:"\ + "+List-Id:+List-Help:+List-Unsubscribe:"\ + "+List-Subscribe:+List-Post:+List-Owner:+List-Archive" + +/* -------------------------------------------------------------------------- */ +/* Length of the preallocated buffer for the "answer" from the dns/txt + callback function. This should match the maximum RDLENGTH from DNS. */ +#define PDKIM_DNS_TXT_MAX_RECLEN (1 << 16) + +/* -------------------------------------------------------------------------- */ +/* Function success / error codes */ +#define PDKIM_OK 0 +#define PDKIM_FAIL -1 +#define PDKIM_ERR_RSA_PRIVKEY -101 +#define PDKIM_ERR_RSA_SIGNING -102 +#define PDKIM_ERR_LONG_LINE -103 +#define PDKIM_ERR_BUFFER_TOO_SMALL -104 +#define PDKIM_ERR_EXCESS_SIGS -105 +#define PDKIM_SIGN_PRIVKEY_WRAP -106 +#define PDKIM_SIGN_PRIVKEY_B64D -107 + +/* -------------------------------------------------------------------------- */ +/* Main/Extended verification status */ +#define PDKIM_VERIFY_NONE 0 +#define PDKIM_VERIFY_INVALID 1 +#define PDKIM_VERIFY_FAIL 2 +#define PDKIM_VERIFY_PASS 3 +#define PDKIM_VERIFY_POLICY BIT(31) + +#define PDKIM_VERIFY_FAIL_BODY 1 +#define PDKIM_VERIFY_FAIL_MESSAGE 2 +#define PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH 3 +#define PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE 4 +#define PDKIM_VERIFY_INVALID_BUFFER_SIZE 5 +#define PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD 6 +#define PDKIM_VERIFY_INVALID_PUBKEY_IMPORT 7 +#define PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE 8 +#define PDKIM_VERIFY_INVALID_SIGNATURE_ERROR 9 +#define PDKIM_VERIFY_INVALID_DKIM_VERSION 10 + +/* -------------------------------------------------------------------------- */ +/* Some parameter values */ +#define PDKIM_QUERYMETHOD_DNS_TXT 0 + +#define PDKIM_CANON_SIMPLE 0 +#define PDKIM_CANON_RELAXED 1 + +/* -------------------------------------------------------------------------- */ +/* Some required forward declarations, please ignore */ +typedef struct pdkim_stringlist pdkim_stringlist; +typedef struct pdkim_str pdkim_str; +typedef struct sha1_context sha1_context; +typedef struct sha2_context sha2_context; +#define HAVE_SHA1_CONTEXT +#define HAVE_SHA2_CONTEXT + +/* -------------------------------------------------------------------------- */ +/* Some concessions towards Redmond */ +#ifdef WINDOWS +#define snprintf _snprintf +#define strcasecmp _stricmp +#define strncasecmp _strnicmp +#define DLLEXPORT __declspec(dllexport) +#else +#define DLLEXPORT +#endif + + +/* -------------------------------------------------------------------------- */ +/* Public key as (usually) fetched from DNS */ +typedef struct pdkim_pubkey { + const uschar * version; /* v= */ + const uschar *granularity; /* g= */ + + const uschar * hashes; /* h= */ + const uschar * keytype; /* k= */ + const uschar * srvtype; /* s= */ + uschar *notes; /* n= */ + + blob key; /* p= */ + int testing; /* t=y */ + int no_subdomaining; /* t=s */ +} pdkim_pubkey; + +/* -------------------------------------------------------------------------- */ +/* Body-hash to be calculated */ +typedef struct pdkim_bodyhash { + struct pdkim_bodyhash * next; + int hashtype; + int canon_method; + long bodylength; + + hctx body_hash_ctx; + unsigned long signed_body_bytes; /* done so far */ + int num_buffered_blanklines; + + blob bh; /* completed hash */ +} pdkim_bodyhash; + +/* -------------------------------------------------------------------------- */ +/* Signature as it appears in a DKIM-Signature header */ +typedef struct pdkim_signature { + struct pdkim_signature * next; + + /* Bits stored in a DKIM signature header --------------------------- */ + + /* (v=) The version, as an integer. Currently, always "1" */ + int version; + + /* (a=) The signature algorithm. */ + int keytype; /* pdkim_keytypes index */ + unsigned keybits; /* size of the key */ + int hashtype; /* pdkim_hashes index */ + + /* (c=x/) Header canonicalization method. Either PDKIM_CANON_SIMPLE + or PDKIM_CANON_RELAXED */ + int canon_headers; + + /* (c=/x) Body canonicalization method. Either PDKIM_CANON_SIMPLE + or PDKIM_CANON_RELAXED */ + int canon_body; + + /* (q=) Query Method. Currently, only PDKIM_QUERYMETHOD_DNS_TXT + is specified */ + int querymethod; + + /* (s=) The selector string as given in the signature */ + uschar *selector; + + /* (d=) The domain as given in the signature */ + uschar *domain; + + /* (i=) The identity as given in the signature */ + uschar *identity; + + /* (t=) Timestamp of signature creation */ + unsigned long created; + + /* (x=) Timestamp of expiry of signature */ + unsigned long expires; + + /* (l=) Amount of hashed body bytes (after canonicalization). Default + is -1. Note: a value of 0 means that the body is unsigned! */ + long bodylength; + + /* (h=) Colon-separated list of header names that are included in the + signature */ + uschar *headernames; + + /* (z=) */ + uschar *copiedheaders; + + /* (b=) Raw signature data, along with its length in bytes */ + blob sighash; + + /* (bh=) Raw body hash data, along with its length in bytes */ + blob bodyhash; + + /* Folded DKIM-Signature: header. Signing only, NULL for verifying. + Ready for insertion into the message. Note: Folded using CRLFTB, + but final line terminator is NOT included. Note2: This buffer is + free()d when you call pdkim_free_ctx(). */ + uschar *signature_header; + + /* The main verification status. Verification only. One of: + + PDKIM_VERIFY_NONE Verification was not attempted. This status + should not appear. + + PDKIM_VERIFY_INVALID There was an error while trying to verify + the signature. A more precise description + is available in verify_ext_status. + + PDKIM_VERIFY_FAIL Verification failed because either the body + hash did not match, or the signature verification + failed. This means the message was modified. + Check verify_ext_status for the exact reason. + + PDKIM_VERIFY_PASS Verification succeeded. + */ + int verify_status; + + /* Extended verification status. Verification only. Depending on the value + of verify_status, it can contain: + + For verify_status == PDKIM_VERIFY_INVALID: + + PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE + Unable to retrieve a public key container. + + PDKIM_VERIFY_INVALID_BUFFER_SIZE + Either the DNS name constructed to retrieve the public key record + does not fit into PDKIM_DNS_TXT_MAX_NAMELEN bytes, or the retrieved + record is longer than PDKIM_DNS_TXT_MAX_RECLEN bytes. + + PDKIM_VERIFY_INVALID_PUBKEY_PARSING + (Syntax) error while parsing the retrieved public key record. + + + For verify_status == PDKIM_VERIFY_FAIL: + + PDKIM_VERIFY_FAIL_BODY + The calculated body hash does not match the advertised body hash + from the bh= tag of the signature. + + PDKIM_VERIFY_FAIL_MESSAGE + RSA verification of the signature (b= tag) failed. + */ + int verify_ext_status; + + /* Pointer to a public key record that was used to verify the signature. + See pdkim_pubkey declaration above for more information. + Caution: is NULL if signing or if no record was retrieved. */ + pdkim_pubkey *pubkey; + + /* Properties below this point are used internally only ------------- */ + + /* Per-signature helper variables ----------------------------------- */ + pdkim_bodyhash *calc_body_hash; /* hash to be / being calculated */ + + pdkim_stringlist *headers; /* Raw headers included in the sig */ + + /* Signing specific ------------------------------------------------- */ + uschar * privkey; /* Private key */ + uschar * sign_headers; /* To-be-signed header names */ + uschar * rawsig_no_b_val; /* Original signature header w/o b= tag value. */ +} pdkim_signature; + + +/* -------------------------------------------------------------------------- */ +/* Context to keep state between all operations. */ +typedef struct pdkim_ctx { + +#define PDKIM_MODE_SIGN BIT(0) /* if unset, mode==verify */ +#define PDKIM_DOT_TERM BIT(1) /* dot termination and unstuffing */ +#define PDKIM_SEEN_CR BIT(2) +#define PDKIM_SEEN_LF BIT(3) +#define PDKIM_PAST_HDRS BIT(4) +#define PDKIM_SEEN_EOD BIT(5) + unsigned flags; + + /* One (signing) or several chained (verification) signatures */ + pdkim_signature *sig; + + /* One (signing) or several chained (verification) bodyhashes */ + pdkim_bodyhash *bodyhash; + + /* Callback for dns/txt query method (verification only) */ + uschar * (*dns_txt_callback)(const uschar *); + + /* Coder's little helpers */ + gstring *cur_header; + uschar *linebuf; + int linebuf_offset; + int num_headers; + pdkim_stringlist *headers; /* Raw headers for verification */ +} pdkim_ctx; + + +/******************************************************************************/ + +typedef struct { + const uschar * dkim_hashname; + hashmethod exim_hashmethod; +} pdkim_hashtype; +extern const pdkim_hashtype pdkim_hashes[]; + +/******************************************************************************/ + + +/* -------------------------------------------------------------------------- */ +/* API functions. Please see the sample code in sample/test_sign.c and + sample/test_verify.c for documentation. +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +void pdkim_init (void); + +void pdkim_init_context (pdkim_ctx *, BOOL, uschar * (*)(const uschar *)); + +DLLEXPORT +pdkim_signature *pdkim_init_sign (pdkim_ctx *, + uschar *, uschar *, uschar *, uschar *, + const uschar **); + +DLLEXPORT +pdkim_ctx *pdkim_init_verify (uschar * (*)(const uschar *), BOOL); + +DLLEXPORT +void pdkim_set_optional (pdkim_signature *, char *, char *,int, int, + long, + unsigned long, + unsigned long); + +int pdkim_hashname_to_hashtype(const uschar *, unsigned); +void pdkim_cstring_to_canons(const uschar *, unsigned, int *, int *); +pdkim_bodyhash *pdkim_set_bodyhash(pdkim_ctx *, int, int, long); +pdkim_bodyhash *pdkim_set_sig_bodyhash(pdkim_ctx *, pdkim_signature *); + +DLLEXPORT +int pdkim_feed (pdkim_ctx *, const uschar *, unsigned); +DLLEXPORT +int pdkim_feed_finish (pdkim_ctx *, pdkim_signature **, const uschar **); + +DLLEXPORT +void pdkim_free_ctx (pdkim_ctx *); + + +const uschar * pdkim_errstr(int); + +extern uschar * pdkim_encode_base64(blob *); +extern void pdkim_decode_base64(const uschar *, blob *); +extern pdkim_pubkey * pdkim_parse_pubkey_record(const uschar *); +extern uschar * pdkim_relax_header_n(const uschar *, int, BOOL); +extern uschar * pdkim_relax_header(const uschar *, BOOL); +extern uschar * dkim_sig_to_a_tag(const pdkim_signature *); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/src/miscmods/pdkim/pdkim_hash.h b/src/src/miscmods/pdkim/pdkim_hash.h new file mode 100644 index 000000000..d56e3ce34 --- /dev/null +++ b/src/src/miscmods/pdkim/pdkim_hash.h @@ -0,0 +1,39 @@ +/* + * PDKIM - a RFC4871 (DKIM) implementation + * + * Copyright (C) 1995 - 2018 Exim maintainers + * SPDX-License-Identifier: GPL-2.0-or-later + * + * Hash interface functions + */ + +#include "../exim.h" + +#if !defined(HASH_H) /* entire file */ +#define HASH_H + +#ifdef DISABLE_TLS +# error Must not DISABLE_TLS, for DKIM +#endif + +#include "crypt_ver.h" +#include "../blob.h" +#include "../hash.h" + +#ifdef SIGN_OPENSSL +# include +# include +# include +#elif defined(SIGN_GNUTLS) +# include +# include +#endif + +#if defined(SHA_OPENSSL) +# include "pdkim.h" +#elif defined(SHA_GCRYPT) +# include "pdkim.h" +#endif + +#endif +/* End of File */ diff --git a/src/src/miscmods/pdkim/signing.c b/src/src/miscmods/pdkim/signing.c new file mode 100644 index 000000000..44f2e12ac --- /dev/null +++ b/src/src/miscmods/pdkim/signing.c @@ -0,0 +1,913 @@ +/* + * PDKIM - a RFC4871 (DKIM) implementation + * Copyright (c) The Exim Maintainers 1995 - 2024 + * SPDX-License-Identifier: GPL-2.0-or-later + * + * signing/verification interface + */ + +#include "../exim.h" +#include "crypt_ver.h" +#include "signing.h" + + +#ifdef MACRO_PREDEF +# include "../macro_predef.h" + +void +features_crypto(void) +{ +# ifdef SIGN_HAVE_ED25519 + builtin_macro_create(US"_CRYPTO_SIGN_ED25519"); +# endif +# ifdef EXIM_HAVE_SHA3 + builtin_macro_create(US"_CRYPTO_HASH_SHA3"); +# endif +} +#else + +#ifndef DISABLE_DKIM /* rest of file */ + +#ifdef DISABLE_TLS +# error Must no DISABLE_TLS, for DKIM +#endif + + +/******************************************************************************/ +#ifdef SIGN_GNUTLS +# define EXIM_GNUTLS_LIBRARY_LOG_LEVEL 3 + +# ifndef GNUTLS_VERIFY_ALLOW_BROKEN +# define GNUTLS_VERIFY_ALLOW_BROKEN 0 +# endif + + +/* Logging function which can be registered with + * gnutls_global_set_log_function() + * gnutls_global_set_log_level() 0..9 + */ +#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 +static void +exim_gnutls_logger_cb(int level, const char *message) +{ +size_t len = strlen(message); +if (len < 1) + { + DEBUG(D_tls) debug_printf("GnuTLS<%d> empty debug message\n", level); + return; + } +DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s%s", level, message, + message[len-1] == '\n' ? "" : "\n"); +} +#endif + + + +void +exim_dkim_signers_init(void) +{ +#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 +DEBUG(D_tls) + { + gnutls_global_set_log_function(exim_gnutls_logger_cb); + /* arbitrarily chosen level; bump upto 9 for more */ + gnutls_global_set_log_level(EXIM_GNUTLS_LIBRARY_LOG_LEVEL); + } +#endif +} + + +/* accumulate data (gnutls-only). String to be appended must be nul-terminated. */ +gstring * +exim_dkim_data_append(gstring * g, uschar * s) +{ +return string_cat(g, s); +} + + + +/* import private key from PEM string in memory. +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_signing_init(const uschar * privkey_pem, es_ctx * sign_ctx) +{ +gnutls_datum_t k = { .data = (void *)privkey_pem, .size = Ustrlen(privkey_pem) }; +gnutls_x509_privkey_t x509_key; +const uschar * where; +int rc; + +if ( (where = US"internal init", rc = gnutls_x509_privkey_init(&x509_key)) + || (rc = gnutls_privkey_init(&sign_ctx->key)) + || (where = US"privkey PEM-block import", + rc = gnutls_x509_privkey_import(x509_key, &k, GNUTLS_X509_FMT_PEM)) + || (where = US"internal privkey transfer", + rc = gnutls_privkey_import_x509(sign_ctx->key, x509_key, 0)) + ) + return string_sprintf("%s: %s", where, gnutls_strerror(rc)); + +switch (rc = gnutls_privkey_get_pk_algorithm(sign_ctx->key, NULL)) + { + case GNUTLS_PK_RSA: sign_ctx->keytype = KEYTYPE_RSA; break; +#ifdef SIGN_HAVE_ED25519 + case GNUTLS_PK_EDDSA_ED25519: sign_ctx->keytype = KEYTYPE_ED25519; break; +#endif + default: return rc < 0 + ? CUS gnutls_strerror(rc) + : string_sprintf("Unhandled key type: %d '%s'", rc, gnutls_pk_get_name(rc)); + } + +return NULL; +} + + + +/* allocate mem for signature (when signing) */ +/* hash & sign data. No way to do incremental. + +Arguments: + sign_ctx library-specific context for a signature (incl. key) + hash hash method to apply to data + data data to be signed + sig returned signature + +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_sign(es_ctx * sign_ctx, hashmethod hash, const blob * data, blob * sig) +{ +gnutls_datum_t k_data = { .data = data->data, .size = data->len }; +gnutls_digest_algorithm_t dig; +gnutls_datum_t k_sig; +int rc; + +switch (hash) + { + case HASH_SHA1: dig = GNUTLS_DIG_SHA1; break; + case HASH_SHA2_256: dig = GNUTLS_DIG_SHA256; break; + case HASH_SHA2_512: dig = GNUTLS_DIG_SHA512; break; + default: return US"nonhandled hash type"; + } + +if ((rc = gnutls_privkey_sign_data(sign_ctx->key, dig, 0, &k_data, &k_sig))) + return CUS gnutls_strerror(rc); + +/* Don't care about deinit for the key; shortlived process */ + +sig->data = k_sig.data; +sig->len = k_sig.size; +return NULL; +} + + + +/* import public key (from blob in memory) +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_verify_init(const blob * pubkey, keyformat fmt, ev_ctx * verify_ctx, + unsigned * bits) +{ +gnutls_datum_t k; +int rc; +const uschar * ret = NULL; + +gnutls_pubkey_init(&verify_ctx->key); +k.data = pubkey->data; +k.size = pubkey->len; + +switch(fmt) + { + case KEYFMT_DER: + if ((rc = gnutls_pubkey_import(verify_ctx->key, &k, GNUTLS_X509_FMT_DER))) + ret = US gnutls_strerror(rc); + break; +#ifdef SIGN_HAVE_ED25519 + case KEYFMT_ED25519_BARE: + if ((rc = gnutls_pubkey_import_ecc_raw(verify_ctx->key, + GNUTLS_ECC_CURVE_ED25519, &k, NULL))) + ret = US gnutls_strerror(rc); + break; +#endif + default: + ret = US"pubkey format not handled"; + break; + } +if (!ret && bits) gnutls_pubkey_get_pk_algorithm(verify_ctx->key, bits); +return ret; +} + + +/* verify signature (of hash if RSA sig, of data if EC sig. No way to do incremental) +(given pubkey & alleged sig) +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_verify(ev_ctx * verify_ctx, hashmethod hash, const blob * data_hash, + const blob * sig) +{ +gnutls_datum_t k = { .data = data_hash->data, .size = data_hash->len }; +gnutls_datum_t s = { .data = sig->data, .size = sig->len }; +int rc; +const uschar * ret = NULL; + +#ifdef SIGN_HAVE_ED25519 +if (verify_ctx->keytype == KEYTYPE_ED25519) + { + if ((rc = gnutls_pubkey_verify_data2(verify_ctx->key, + GNUTLS_SIGN_EDDSA_ED25519, 0, &k, &s)) < 0) + ret = US gnutls_strerror(rc); + } +else +#endif + { + gnutls_sign_algorithm_t algo; + switch (hash) + { + case HASH_SHA1: algo = GNUTLS_SIGN_RSA_SHA1; break; + case HASH_SHA2_256: algo = GNUTLS_SIGN_RSA_SHA256; break; + case HASH_SHA2_512: algo = GNUTLS_SIGN_RSA_SHA512; break; + default: return US"nonhandled hash type"; + } + + if ((rc = gnutls_pubkey_verify_hash2(verify_ctx->key, algo, + GNUTLS_VERIFY_ALLOW_BROKEN, &k, &s)) < 0) + ret = US gnutls_strerror(rc); + } + +gnutls_pubkey_deinit(verify_ctx->key); +return ret; +} + + + + +#elif defined(SIGN_GCRYPT) +/******************************************************************************/ +/* This variant is used under pre-3.0.0 GnuTLS. Only rsa-sha1 and rsa-sha256 */ + + +/* Internal service routine: +Read and move past an asn.1 header, checking class & tag, +optionally returning the data-length */ + +static int +as_tag(blob * der, uschar req_cls, long req_tag, long * alen) +{ +int rc; +uschar tag_class; +int taglen; +long tag, len; + +debug_printf_indent("as_tag: %02x %02x %02x %02x\n", + der->data[0], der->data[1], der->data[2], der->data[3]); + +if ((rc = asn1_get_tag_der(der->data++, der->len--, &tag_class, &taglen, &tag)) + != ASN1_SUCCESS) + return rc; + +if (tag_class != req_cls || tag != req_tag) return ASN1_ELEMENT_NOT_FOUND; + +if ((len = asn1_get_length_der(der->data, der->len, &taglen)) < 0) + return ASN1_DER_ERROR; +if (alen) *alen = len; + +/* debug_printf_indent("as_tag: tlen %d dlen %d\n", taglen, (int)len); */ + +der->data += taglen; +der->len -= taglen; +return rc; +} + +/* Internal service routine: +Read and move over an asn.1 integer, setting an MPI to the value +*/ + +static uschar * +as_mpi(blob * der, gcry_mpi_t * mpi) +{ +long alen; +int rc; +gcry_error_t gerr; + +debug_printf_indent("%s\n", __FUNCTION__); + +/* integer; move past the header */ +if ((rc = as_tag(der, 0, ASN1_TAG_INTEGER, &alen)) != ASN1_SUCCESS) + return US asn1_strerror(rc); + +/* read to an MPI */ +if ((gerr = gcry_mpi_scan(mpi, GCRYMPI_FMT_STD, der->data, alen, NULL))) + return US gcry_strerror(gerr); + +/* move over the data */ +der->data += alen; der->len -= alen; +return NULL; +} + + + +void +exim_dkim_signers_init(void) +{ +/* Version check should be the very first call because it +makes sure that important subsystems are initialized. */ +if (!gcry_check_version (GCRYPT_VERSION)) + { + fputs ("libgcrypt version mismatch\n", stderr); + exim_exit(2); + } + +/* We don't want to see any warnings, e.g. because we have not yet +parsed program options which might be used to suppress such +warnings. */ +gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN); + +/* ... If required, other initialization goes here. Note that the +process might still be running with increased privileges and that +the secure memory has not been initialized. */ + +/* Allocate a pool of 16k secure memory. This make the secure memory +available and also drops privileges where needed. */ +gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0); + +/* It is now okay to let Libgcrypt complain when there was/is +a problem with the secure memory. */ +gcry_control (GCRYCTL_RESUME_SECMEM_WARN); + +/* ... If required, other initialization goes here. */ + +/* Tell Libgcrypt that initialization has completed. */ +gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); + +return; +} + + + + +/* Accumulate data (gnutls-only). +String to be appended must be nul-terminated. */ + +gstring * +exim_dkim_data_append(gstring * g, uschar * s) +{ +return g; /*dummy*/ +} + + + +/* import private key from PEM string in memory. +Only handles RSA keys. +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_signing_init(const uschar * privkey_pem, es_ctx * sign_ctx) +{ +uschar * s1, * s2; +blob der; +long alen; +int rc; + +/*XXX will need extension to _spot_ as well as handle a +non-RSA key? I think... +So... this is not a PrivateKeyInfo - which would have a field +identifying the keytype - PrivateKeyAlgorithmIdentifier - +but a plain RSAPrivateKey (wrapped in PEM-headers. Can we +use those as a type tag? What forms are there? "BEGIN EC PRIVATE KEY" (cf. ec(1ssl)) + +How does OpenSSL PEM_read_bio_PrivateKey() deal with it? +gnutls_x509_privkey_import() ? +*/ + +/* + * RSAPrivateKey ::= SEQUENCE + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p-1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * otherPrimeInfos OtherPrimeInfos OPTIONAL + + * ECPrivateKey ::= SEQUENCE { + * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + * privateKey OCTET STRING, + * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + * publicKey [1] BIT STRING OPTIONAL + * } + * Hmm, only 1 useful item, and not even an integer? Wonder how we might use it... + +- actually, gnutls_x509_privkey_import() appears to require a curve name parameter + value for that is an OID? a local-only integer (it's an enum in GnuTLS)? + + +Useful cmds: + ssh-keygen -t ecdsa -f foo.privkey + ssh-keygen -t ecdsa -b384 -f foo.privkey + ssh-keygen -t ecdsa -b521 -f foo.privkey + ssh-keygen -t ed25519 -f foo.privkey + + < foo openssl pkcs8 -in /dev/stdin -inform PEM -nocrypt -topk8 -outform DER | od -x + + openssl asn1parse -in foo -inform PEM -dump + openssl asn1parse -in foo -inform PEM -dump -stroffset 24 (??) +(not good for ed25519) + + */ + +if ( !(s1 = Ustrstr(CS privkey_pem, "-----BEGIN RSA PRIVATE KEY-----")) + || !(s2 = Ustrstr(CS (s1+=31), "-----END RSA PRIVATE KEY-----" )) + ) + return US"Bad PEM wrapper"; + +*s2 = '\0'; + +if ((rc = b64decode(s1, &der.data, s1) < 0)) + return US"Bad PEM-DER b64 decode"; +der.len = rc; + +/* untangle asn.1 */ + +/* sequence; just move past the header */ +if ((rc = as_tag(&der, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, NULL)) + != ASN1_SUCCESS) goto asn_err; + +/* integer version; move past the header, check is zero */ +if ((rc = as_tag(&der, 0, ASN1_TAG_INTEGER, &alen)) != ASN1_SUCCESS) + goto asn_err; +if (alen != 1 || *der.data != 0) + return US"Bad version number"; +der.data++; der.len--; + +if ( (s1 = as_mpi(&der, &sign_ctx->n)) + || (s1 = as_mpi(&der, &sign_ctx->e)) + || (s1 = as_mpi(&der, &sign_ctx->d)) + || (s1 = as_mpi(&der, &sign_ctx->p)) + || (s1 = as_mpi(&der, &sign_ctx->q)) + || (s1 = as_mpi(&der, &sign_ctx->dp)) + || (s1 = as_mpi(&der, &sign_ctx->dq)) + || (s1 = as_mpi(&der, &sign_ctx->qp)) + ) + return s1; + +#ifdef extreme_debug +DEBUG(D_acl) debug_printf_indent("rsa_signing_init:\n"); + { + uschar * s; + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->n); + debug_printf_indent(" N : %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->e); + debug_printf_indent(" E : %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->d); + debug_printf_indent(" D : %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->p); + debug_printf_indent(" P : %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->q); + debug_printf_indent(" Q : %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->dp); + debug_printf_indent(" DP: %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->dq); + debug_printf_indent(" DQ: %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->qp); + debug_printf_indent(" QP: %s\n", s); + } +#endif + +sign_ctx->keytype = KEYTYPE_RSA; +return NULL; + +asn_err: return US asn1_strerror(rc); +} + + + +/* allocate mem for signature (when signing) */ +/* sign already-hashed data. + +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_sign(es_ctx * sign_ctx, hashmethod hash, const blob * data, blob * sig) +{ +char * sexp_hash; +gcry_sexp_t s_hash = NULL, s_key = NULL, s_sig = NULL; +gcry_mpi_t m_sig; +uschar * errstr; +gcry_error_t gerr; + +/*XXX will need extension for hash types (though, possibly, should +be re-specced to not rehash but take an already-hashed value? Actually +current impl looks WRONG - it _is_ given a hash so should not be +re-hashing. Has this been tested? + +Will need extension for non-RSA sugning algos. */ + +switch (hash) + { + case HASH_SHA1: sexp_hash = "(data(flags pkcs1)(hash sha1 %b))"; break; + case HASH_SHA2_256: sexp_hash = "(data(flags pkcs1)(hash sha256 %b))"; break; + default: return US"nonhandled hash type"; + } + +#define SIGSPACE 128 +sig->data = store_get(SIGSPACE, GET_UNTAINTED); + +if (gcry_mpi_cmp (sign_ctx->p, sign_ctx->q) > 0) + { + gcry_mpi_swap (sign_ctx->p, sign_ctx->q); + gcry_mpi_invm (sign_ctx->qp, sign_ctx->p, sign_ctx->q); + } + +if ( (gerr = gcry_sexp_build (&s_key, NULL, + "(private-key (rsa (n%m)(e%m)(d%m)(p%m)(q%m)(u%m)))", + sign_ctx->n, sign_ctx->e, + sign_ctx->d, sign_ctx->p, + sign_ctx->q, sign_ctx->qp)) + || (gerr = gcry_sexp_build (&s_hash, NULL, sexp_hash, + (int) data->len, CS data->data)) + || (gerr = gcry_pk_sign (&s_sig, s_hash, s_key)) + ) + return US gcry_strerror(gerr); + +/* gcry_sexp_dump(s_sig); */ + +if ( !(s_sig = gcry_sexp_find_token(s_sig, "s", 0)) + ) + return US"no sig result"; + +m_sig = gcry_sexp_nth_mpi(s_sig, 1, GCRYMPI_FMT_USG); + +#ifdef extreme_debug +DEBUG(D_acl) + { + uschar * s; + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, m_sig); + debug_printf_indent(" SG: %s\n", s); + } +#endif + +gerr = gcry_mpi_print(GCRYMPI_FMT_USG, sig->data, SIGSPACE, &sig->len, m_sig); +if (gerr) + { + debug_printf_indent("signature conversion from MPI to buffer failed\n"); + return US gcry_strerror(gerr); + } +#undef SIGSPACE + +return NULL; +} + + +/* import public key (from blob in memory) +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_verify_init(const blob * pubkey, keyformat fmt, ev_ctx * verify_ctx, + unsigned * bits) +{ +/* +in code sequence per b81207d2bfa92 rsa_parse_public_key() and asn1_get_mpi() +*/ +uschar tag_class; +int taglen; +long alen; +unsigned nbits; +int rc; +uschar * errstr; +gcry_error_t gerr; +uschar * stage = US"S1"; + +if (fmt != KEYFMT_DER) return US"pubkey format not handled"; + +/* +sequence + sequence + OBJECT:rsaEncryption + NULL + BIT STRING:RSAPublicKey + sequence + INTEGER:Public modulus + INTEGER:Public exponent + +openssl rsa -in aux-fixed/dkim/dkim.private -pubout -outform DER | od -t x1 | head; +openssl rsa -in aux-fixed/dkim/dkim.private -pubout | openssl asn1parse -dump; +openssl rsa -in aux-fixed/dkim/dkim.private -pubout | openssl asn1parse -dump -offset 22; +*/ + +/* sequence; just move past the header */ +if ((rc = as_tag(pubkey, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, NULL)) + != ASN1_SUCCESS) goto asn_err; + +/* sequence; skip the entire thing */ +DEBUG(D_acl) stage = US"S2"; +if ((rc = as_tag(pubkey, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, &alen)) + != ASN1_SUCCESS) goto asn_err; +pubkey->data += alen; pubkey->len -= alen; + + +/* bitstring: limit range to size of bitstring; +move over header + content wrapper */ +DEBUG(D_acl) stage = US"BS"; +if ((rc = as_tag(pubkey, 0, ASN1_TAG_BIT_STRING, &alen)) != ASN1_SUCCESS) + goto asn_err; +pubkey->len = alen; +pubkey->data++; pubkey->len--; + +/* sequence; just move past the header */ +DEBUG(D_acl) stage = US"S3"; +if ((rc = as_tag(pubkey, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, NULL)) + != ASN1_SUCCESS) goto asn_err; + +/* read two integers */ +DEBUG(D_acl) stage = US"MPI"; +nbits = pubkey->len; +if ((errstr = as_mpi(pubkey, &verify_ctx->n))) return errstr; +nbits = (nbits - pubkey->len) * 8; +if ((errstr = as_mpi(pubkey, &verify_ctx->e))) return errstr; + +#ifdef extreme_debug +DEBUG(D_acl) debug_printf_indent("rsa_verify_init:\n"); + { + uschar * s; + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, verify_ctx->n); + debug_printf_indent(" N : %s\n", s); + gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, verify_ctx->e); + debug_printf_indent(" E : %s\n", s); + } + +#endif +if (bits) *bits = nbits; +return NULL; + +asn_err: +DEBUG(D_acl) return string_sprintf("%s: %s", stage, asn1_strerror(rc)); + return US asn1_strerror(rc); +} + + +/* verify signature (of hash) +XXX though we appear to be doing a hash, too! +(given pubkey & alleged sig) +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_verify(ev_ctx * verify_ctx, hashmethod hash, const blob * data_hash, + const blob * sig) +{ +/* +cf. libgnutls 2.8.5 _wrap_gcry_pk_verify() +*/ +char * sexp_hash; +gcry_mpi_t m_sig; +gcry_sexp_t s_sig = NULL, s_hash = NULL, s_pkey = NULL; +gcry_error_t gerr; +uschar * stage; + +/*XXX needs extension for SHA512 */ +switch (hash) + { + case HASH_SHA1: sexp_hash = "(data(flags pkcs1)(hash sha1 %b))"; break; + case HASH_SHA2_256: sexp_hash = "(data(flags pkcs1)(hash sha256 %b))"; break; + default: return US"nonhandled hash type"; + } + +if ( (stage = US"pkey sexp build", + gerr = gcry_sexp_build (&s_pkey, NULL, "(public-key(rsa(n%m)(e%m)))", + verify_ctx->n, verify_ctx->e)) + || (stage = US"data sexp build", + gerr = gcry_sexp_build (&s_hash, NULL, sexp_hash, + (int) data_hash->len, CS data_hash->data)) + || (stage = US"sig mpi scan", + gerr = gcry_mpi_scan(&m_sig, GCRYMPI_FMT_USG, sig->data, sig->len, NULL)) + || (stage = US"sig sexp build", + gerr = gcry_sexp_build (&s_sig, NULL, "(sig-val(rsa(s%m)))", m_sig)) + || (stage = US"verify", + gerr = gcry_pk_verify (s_sig, s_hash, s_pkey)) + ) + { + DEBUG(D_acl) debug_printf_indent("verify: error in stage '%s'\n", stage); + return US gcry_strerror(gerr); + } + +if (s_sig) gcry_sexp_release (s_sig); +if (s_hash) gcry_sexp_release (s_hash); +if (s_pkey) gcry_sexp_release (s_pkey); +gcry_mpi_release (m_sig); +gcry_mpi_release (verify_ctx->n); +gcry_mpi_release (verify_ctx->e); + +return NULL; +} + + + + +#elif defined(SIGN_OPENSSL) +/******************************************************************************/ + +void +exim_dkim_signers_init(void) +{ +ERR_load_crypto_strings(); +} + + +/* accumulate data (was gnutls-only but now needed for OpenSSL non-EC too +because now using hash-and-sign interface) */ +gstring * +exim_dkim_data_append(gstring * g, uschar * s) +{ +return string_cat(g, s); +} + + +/* import private key from PEM string in memory. +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_signing_init(const uschar * privkey_pem, es_ctx * sign_ctx) +{ +BIO * bp = BIO_new_mem_buf((void *)privkey_pem, -1); + +if (!(sign_ctx->key = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL))) + return string_sprintf("privkey PEM-block import: %s", + ERR_error_string(ERR_get_error(), NULL)); + +sign_ctx->keytype = +#ifdef SIGN_HAVE_ED25519 + EVP_PKEY_type(EVP_PKEY_id(sign_ctx->key)) == EVP_PKEY_ED25519 + ? KEYTYPE_ED25519 : KEYTYPE_RSA; +#else + KEYTYPE_RSA; +#endif +return NULL; +} + + + +/* allocate mem for signature (when signing) */ +/* hash & sign data. Incremental not supported. + +Return: NULL for success with the signaature in the sig blob, or an error string */ + +const uschar * +exim_dkim_sign(es_ctx * sign_ctx, hashmethod hash, const blob * data, blob * sig) +{ +const EVP_MD * md; +EVP_MD_CTX * ctx; +size_t siglen; + +switch (hash) + { + case HASH_NULL: md = NULL; break; /* Ed25519 signing */ + case HASH_SHA1: md = EVP_sha1(); break; + case HASH_SHA2_256: md = EVP_sha256(); break; + case HASH_SHA2_512: md = EVP_sha512(); break; + default: return US"nonhandled hash type"; + } + +#ifdef SIGN_HAVE_ED25519 +if ( (ctx = EVP_MD_CTX_new()) + && EVP_DigestSignInit(ctx, NULL, md, NULL, sign_ctx->key) > 0 + && EVP_DigestSign(ctx, NULL, &siglen, NULL, 0) > 0 + && (sig->data = store_get(siglen, GET_UNTAINTED)) + + /* Obtain the signature (slen could change here!) */ + && EVP_DigestSign(ctx, sig->data, &siglen, data->data, data->len) > 0 + ) + { + EVP_MD_CTX_destroy(ctx); + sig->len = siglen; + return NULL; + } +#else +/*XXX renamed to EVP_MD_CTX_new() in 1.1.0 */ +if ( (ctx = EVP_MD_CTX_create()) + && EVP_DigestSignInit(ctx, NULL, md, NULL, sign_ctx->key) > 0 + && EVP_DigestSignUpdate(ctx, data->data, data->len) > 0 + && EVP_DigestSignFinal(ctx, NULL, &siglen) > 0 + && (sig->data = store_get(siglen, GET_UNTAINTED)) + + /* Obtain the signature (slen could change here!) */ + && EVP_DigestSignFinal(ctx, sig->data, &siglen) > 0 + ) + { + EVP_MD_CTX_destroy(ctx); + sig->len = siglen; + return NULL; + } +#endif + +if (ctx) EVP_MD_CTX_destroy(ctx); +return US ERR_error_string(ERR_get_error(), NULL); +} + + + +/* import public key (from blob in memory) +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_verify_init(const blob * pubkey, keyformat fmt, ev_ctx * verify_ctx, + unsigned * bits) +{ +const uschar * s = pubkey->data; +uschar * ret = NULL; + +switch(fmt) + { + case KEYFMT_DER: + /*XXX hmm, we never free this */ + if (!(verify_ctx->key = d2i_PUBKEY(NULL, &s, pubkey->len))) + ret = US ERR_error_string(ERR_get_error(), NULL); + break; +#ifdef SIGN_HAVE_ED25519 + case KEYFMT_ED25519_BARE: + if (!(verify_ctx->key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, + s, pubkey->len))) + ret = US ERR_error_string(ERR_get_error(), NULL); + break; +#endif + default: + ret = US"pubkey format not handled"; + break; + } + +if (!ret && bits) *bits = EVP_PKEY_bits(verify_ctx->key); +return ret; +} + + + + +/* verify signature (of hash, except Ed25519 where of-data) +(given pubkey & alleged sig) +Return: NULL for success, or an error string */ + +const uschar * +exim_dkim_verify(ev_ctx * verify_ctx, hashmethod hash, const blob * data, + const blob * sig) +{ +const EVP_MD * md; + +switch (hash) + { + case HASH_NULL: md = NULL; break; + case HASH_SHA1: md = EVP_sha1(); break; + case HASH_SHA2_256: md = EVP_sha256(); break; + case HASH_SHA2_512: md = EVP_sha512(); break; + default: return US"nonhandled hash type"; + } + +#ifdef SIGN_HAVE_ED25519 +if (!md) + { + EVP_MD_CTX * ctx; + + if ((ctx = EVP_MD_CTX_new())) + { + if ( EVP_DigestVerifyInit(ctx, NULL, md, NULL, verify_ctx->key) > 0 + && EVP_DigestVerify(ctx, sig->data, sig->len, data->data, data->len) > 0 + ) + { EVP_MD_CTX_free(ctx); return NULL; } + EVP_MD_CTX_free(ctx); + } + } +else +#endif + { + EVP_PKEY_CTX * ctx; + + if ((ctx = EVP_PKEY_CTX_new(verify_ctx->key, NULL))) + { + if ( EVP_PKEY_verify_init(ctx) > 0 + && EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) > 0 + && EVP_PKEY_CTX_set_signature_md(ctx, md) > 0 + && EVP_PKEY_verify(ctx, sig->data, sig->len, + data->data, data->len) == 1 + ) + { EVP_PKEY_CTX_free(ctx); return NULL; } + EVP_PKEY_CTX_free(ctx); + + DEBUG(D_tls) + if (Ustrcmp(ERR_reason_error_string(ERR_peek_error()), "wrong signature length") == 0) + debug_printf("sig len (from msg hdr): %d, expected (from dns pubkey) %d\n", + (int) sig->len, EVP_PKEY_size(verify_ctx->key)); + } + } + +return US ERR_error_string(ERR_get_error(), NULL); +} + + + +#endif +/******************************************************************************/ + +#endif /*DISABLE_DKIM*/ +#endif /*MACRO_PREDEF*/ +/* End of File */ diff --git a/src/src/miscmods/pdkim/signing.h b/src/src/miscmods/pdkim/signing.h new file mode 100644 index 000000000..ce801a395 --- /dev/null +++ b/src/src/miscmods/pdkim/signing.h @@ -0,0 +1,100 @@ +/* + * PDKIM - a RFC4871 (DKIM) implementation + * + * Copyright (C) 1995 - 2020 Exim maintainers + * SPDX-License-Identifier: GPL-2.0-or-later + * + * RSA signing/verification interface + */ + +#include "../exim.h" + +#ifndef DISABLE_DKIM /* entire file */ + +#include "crypt_ver.h" + +#ifdef SIGN_OPENSSL +# include +# include +# include +#elif defined(SIGN_GNUTLS) +# include +# include +# include +#elif defined(SIGN_GCRYPT) +# include +# include +#endif + +#include "../blob.h" + +typedef enum { + KEYTYPE_RSA, + KEYTYPE_ED25519 +} keytype; + +typedef enum { + KEYFMT_DER, /* an asn.1 structure */ + KEYFMT_ED25519_BARE /* just the key */ +} keyformat; + + +#ifdef SIGN_OPENSSL + +typedef struct { + keytype keytype; + EVP_PKEY * key; +} es_ctx; + +typedef struct { + keytype keytype; + EVP_PKEY * key; +} ev_ctx; + +#elif defined(SIGN_GNUTLS) + +typedef struct { + keytype keytype; + gnutls_privkey_t key; +} es_ctx; + +typedef struct { + keytype keytype; + gnutls_pubkey_t key; +} ev_ctx; + +#elif defined(SIGN_GCRYPT) + +typedef struct { + keytype keytype; + gcry_mpi_t n; + gcry_mpi_t e; + gcry_mpi_t d; + gcry_mpi_t p; + gcry_mpi_t q; + gcry_mpi_t dp; + gcry_mpi_t dq; + gcry_mpi_t qp; +} es_ctx; + +typedef struct { + keytype keytype; + gcry_mpi_t n; + gcry_mpi_t e; +} ev_ctx; + +#endif + + +extern void exim_dkim_signers_init(void); +extern gstring * exim_dkim_data_append(gstring *, uschar *); + +extern const uschar * exim_dkim_signing_init(const uschar *, es_ctx *); +extern const uschar * exim_dkim_sign(es_ctx *, hashmethod, const blob *, blob *); +extern const uschar * exim_dkim_verify_init(const blob *, keyformat, ev_ctx *, + unsigned *); +extern const uschar * exim_dkim_verify(ev_ctx *, hashmethod, const blob *, + const blob *); + +#endif /*DISABLE_DKIM*/ +/* End of File */ diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c index ea23c1c65..14937e799 100644 --- a/src/src/miscmods/spf.c +++ b/src/src/miscmods/spf.c @@ -566,7 +566,6 @@ static optionlist spf_options[] = { static void * spf_functions[] = { [SPF_PROCESS] = spf_process, - [SPF_AUTHRES] = authres_spf, [SPF_GET_RESPONSE] = spf_get_response, /* ugly; for dmarc */ [SPF_OPEN] = spf_lookup_open, @@ -593,6 +592,7 @@ misc_module_info spf_module_info = .lib_vers_report = spf_lib_version_report, .conn_init = spf_conn_init, .smtp_reset = spf_smtp_reset, + .authres = authres_spf, .options = spf_options, .options_count = nelem(spf_options), diff --git a/src/src/miscmods/spf_api.h b/src/src/miscmods/spf_api.h index 3801e3e3f..117a7ae6a 100644 --- a/src/src/miscmods/spf_api.h +++ b/src/src/miscmods/spf_api.h @@ -6,13 +6,12 @@ /* See the file NOTICE for conditions of use and distribution. */ /* SPDX-License-Identifier: GPL-2.0-or-later */ -/* API definitions for the spfmodule */ +/* API definitions for the spf module */ /* Function table entry numbers */ #define SPF_PROCESS 0 -#define SPF_AUTHRES 1 #define SPF_GET_RESPONSE 2 #define SPF_OPEN 3 #define SPF_CLOSE 4 diff --git a/src/src/pdkim/Makefile b/src/src/pdkim/Makefile deleted file mode 100644 index 47f92eed4..000000000 --- a/src/src/pdkim/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -# Make file for building the pdkim library. -# Copyright (c) The Exim Maintainers 1995 - 2018 - -OBJ = pdkim.o signing.o - -pdkim.a: $(OBJ) - @$(RM_COMMAND) -f pdkim.a - @echo "$(AR) pdkim.a" - $(FE)$(AR) pdkim.a $(OBJ) - $(RANLIB) $@ - -.SUFFIXES: .o .c -.c.o:; @echo "$(CC) $*.c" - $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -I. $*.c - -pdkim.o: $(HDRS) crypt_ver.h pdkim.h pdkim.c -signing.o: $(HDRS) crypt_ver.h signing.h signing.c - -# End diff --git a/src/src/pdkim/README b/src/src/pdkim/README deleted file mode 100644 index 953e86eae..000000000 --- a/src/src/pdkim/README +++ /dev/null @@ -1,9 +0,0 @@ -PDKIM - a RFC4871 (DKIM) implementation -http://duncanthrax.net/pdkim/ -Copyright (C) 2009 Tom Kistner - -No longer includes code from the PolarSSL project. -Copyright (C) 2016 Jeremy Harris - -This copy of PDKIM is included with Exim. For a standalone distribution, -visit http://duncanthrax.net/pdkim/. diff --git a/src/src/pdkim/config.h b/src/src/pdkim/config.h deleted file mode 100644 index fdd4cfe4f..000000000 --- a/src/src/pdkim/config.h +++ /dev/null @@ -1,4 +0,0 @@ -#define POLARSSL_BASE64_C - - - diff --git a/src/src/pdkim/crypt_ver.h b/src/src/pdkim/crypt_ver.h deleted file mode 100644 index 56ae236c1..000000000 --- a/src/src/pdkim/crypt_ver.h +++ /dev/null @@ -1,34 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ - -/* Copyright (c) Jeremy Harris 1995 - 2018 */ -/* See the file NOTICE for conditions of use and distribution. */ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -/* Signing and hashing routine selection for PDKIM */ - -#include "../exim.h" -#include "../sha_ver.h" - - -#ifdef USE_GNUTLS -# include - -# if GNUTLS_VERSION_NUMBER >= 0x030000 -# define SIGN_GNUTLS -# if GNUTLS_VERSION_NUMBER >= 0x030600 -# define SIGN_HAVE_ED25519 -# endif -# else -# define SIGN_GCRYPT -# endif -#endif - -#ifdef USE_OPENSSL -# define SIGN_OPENSSL -# if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10101000L -# define SIGN_HAVE_ED25519 -# endif -#endif - diff --git a/src/src/pdkim/pdkim.c b/src/src/pdkim/pdkim.c deleted file mode 100644 index 42e67e6aa..000000000 --- a/src/src/pdkim/pdkim.c +++ /dev/null @@ -1,2081 +0,0 @@ -/* - * PDKIM - a RFC4871 (DKIM) implementation - * - * Copyright (c) The Exim Maintainers 2021 - 2024 - * Copyright (C) 2016 - 2020 Jeremy Harris - * Copyright (C) 2009 - 2016 Tom Kistner - * SPDX-License-Identifier: GPL-2.0-or-later - * - * http://duncanthrax.net/pdkim/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "../exim.h" - - -#ifndef DISABLE_DKIM /* entire file */ - -#ifdef DISABLE_TLS -# error Must not DISABLE_TLS, for DKIM -#endif - -#include "crypt_ver.h" - -#ifdef SIGN_OPENSSL -# include -# include -# include -#elif defined(SIGN_GNUTLS) -# include -# include -#endif - -#include "pdkim.h" -#include "signing.h" - -#define PDKIM_SIGNATURE_VERSION "1" -#define PDKIM_PUB_RECORD_VERSION US "DKIM1" - -#define PDKIM_MAX_HEADER_LEN 65536 -#define PDKIM_MAX_HEADERS 512 -#define PDKIM_MAX_BODY_LINE_LEN 16384 -#define PDKIM_DNS_TXT_MAX_NAMELEN 1024 - -/* -------------------------------------------------------------------------- */ -struct pdkim_stringlist { - uschar * value; - int tag; - void * next; -}; - -/* -------------------------------------------------------------------------- */ -/* A bunch of list constants */ -const uschar * pdkim_querymethods[] = { - US"dns/txt", - NULL -}; -const uschar * pdkim_canons[] = { - US"simple", - US"relaxed", - NULL -}; - -const pdkim_hashtype pdkim_hashes[] = { - { US"sha1", HASH_SHA1 }, - { US"sha256", HASH_SHA2_256 }, - { US"sha512", HASH_SHA2_512 } -}; - -const uschar * pdkim_keytypes[] = { - [KEYTYPE_RSA] = US"rsa", -#ifdef SIGN_HAVE_ED25519 - [KEYTYPE_ED25519] = US"ed25519", /* Works for 3.6.0 GnuTLS, OpenSSL 1.1.1 */ -#endif - -#ifdef notyet_EC_dkim_extensions /* https://tools.ietf.org/html/draft-srose-dkim-ecc-00 */ - US"eccp256", - US"eccp348", - US"ed448", -#endif -}; - -typedef struct pdkim_combined_canon_entry { - const uschar * str; - int canon_headers; - int canon_body; -} pdkim_combined_canon_entry; - -pdkim_combined_canon_entry pdkim_combined_canons[] = { - { US"simple/simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, - { US"simple/relaxed", PDKIM_CANON_SIMPLE, PDKIM_CANON_RELAXED }, - { US"relaxed/simple", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, - { US"relaxed/relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_RELAXED }, - { US"simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, - { US"relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, - { NULL, 0, 0 } -}; - - -static const blob lineending = {.data = US"\r\n", .len = 2}; - -/* -------------------------------------------------------------------------- */ -uschar * -dkim_sig_to_a_tag(const pdkim_signature * sig) -{ -if ( sig->keytype < 0 || sig->keytype > nelem(pdkim_keytypes) - || sig->hashtype < 0 || sig->hashtype > nelem(pdkim_hashes)) - return US"err"; -return string_sprintf("%s-%s", - pdkim_keytypes[sig->keytype], pdkim_hashes[sig->hashtype].dkim_hashname); -} - - -static int -pdkim_keyname_to_keytype(const uschar * s) -{ -for (int i = 0; i < nelem(pdkim_keytypes); i++) - if (Ustrcmp(s, pdkim_keytypes[i]) == 0) return i; -return -1; -} - -int -pdkim_hashname_to_hashtype(const uschar * s, unsigned len) -{ -if (!len) len = Ustrlen(s); -for (int i = 0; i < nelem(pdkim_hashes); i++) - if (Ustrncmp(s, pdkim_hashes[i].dkim_hashname, len) == 0) - return i; -return -1; -} - -void -pdkim_cstring_to_canons(const uschar * s, unsigned len, - int * canon_head, int * canon_body) -{ -if (!len) len = Ustrlen(s); -for (int i = 0; pdkim_combined_canons[i].str; i++) - if ( Ustrncmp(s, pdkim_combined_canons[i].str, len) == 0 - && len == Ustrlen(pdkim_combined_canons[i].str)) - { - *canon_head = pdkim_combined_canons[i].canon_headers; - *canon_body = pdkim_combined_canons[i].canon_body; - break; - } -} - - - -const char * -pdkim_verify_status_str(int status) -{ -switch(status) - { - case PDKIM_VERIFY_NONE: return "PDKIM_VERIFY_NONE"; - case PDKIM_VERIFY_INVALID: return "PDKIM_VERIFY_INVALID"; - case PDKIM_VERIFY_FAIL: return "PDKIM_VERIFY_FAIL"; - case PDKIM_VERIFY_PASS: return "PDKIM_VERIFY_PASS"; - default: return "PDKIM_VERIFY_UNKNOWN"; - } -} - -const char * -pdkim_verify_ext_status_str(int ext_status) -{ -switch(ext_status) - { - case PDKIM_VERIFY_FAIL_BODY: return "PDKIM_VERIFY_FAIL_BODY"; - case PDKIM_VERIFY_FAIL_MESSAGE: return "PDKIM_VERIFY_FAIL_MESSAGE"; - case PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH: return "PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH"; - case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: return "PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE"; - case PDKIM_VERIFY_INVALID_BUFFER_SIZE: return "PDKIM_VERIFY_INVALID_BUFFER_SIZE"; - case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: return "PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD"; - case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return "PDKIM_VERIFY_INVALID_PUBKEY_IMPORT"; - case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: return "PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE"; - case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: return "PDKIM_VERIFY_INVALID_SIGNATURE_ERROR"; - case PDKIM_VERIFY_INVALID_DKIM_VERSION: return "PDKIM_VERIFY_INVALID_DKIM_VERSION"; - default: return "PDKIM_VERIFY_UNKNOWN"; - } -} - -const uschar * -pdkim_errstr(int status) -{ -switch(status) - { - case PDKIM_OK: return US"OK"; - case PDKIM_FAIL: return US"FAIL"; - case PDKIM_ERR_RSA_PRIVKEY: return US"PRIVKEY"; - case PDKIM_ERR_RSA_SIGNING: return US"SIGNING"; - case PDKIM_ERR_LONG_LINE: return US"LONG_LINE"; - case PDKIM_ERR_BUFFER_TOO_SMALL: return US"BUFFER_TOO_SMALL"; - case PDKIM_ERR_EXCESS_SIGS: return US"EXCESS_SIGS"; - case PDKIM_SIGN_PRIVKEY_WRAP: return US"PRIVKEY_WRAP"; - case PDKIM_SIGN_PRIVKEY_B64D: return US"PRIVKEY_B64D"; - default: return US"(unknown)"; - } -} - - -/* -------------------------------------------------------------------------- */ - - -static pdkim_stringlist * -pdkim_prepend_stringlist(pdkim_stringlist * base, const uschar * str) -{ -pdkim_stringlist * new_entry = store_get(sizeof(pdkim_stringlist), GET_UNTAINTED); - -memset(new_entry, 0, sizeof(pdkim_stringlist)); -new_entry->value = string_copy(str); -if (base) new_entry->next = base; -return new_entry; -} - - - -/* Trim whitespace fore & aft */ - -static void -pdkim_strtrim(gstring * str) -{ -uschar * p = str->s; -uschar * q; - -while (*p == '\t' || *p == ' ') /* dump the leading whitespace */ - { str->size--; str->ptr--; str->s++; } - -while ( str->ptr > 0 - && ((q = str->s + str->ptr - 1), (*q == '\t' || *q == ' ')) - ) - str->ptr--; /* dump trailing whitespace */ - -(void) string_from_gstring(str); -} - - - -/* -------------------------------------------------------------------------- */ - -DLLEXPORT void -pdkim_free_ctx(pdkim_ctx *ctx) -{ -} - - -/* -------------------------------------------------------------------------- */ -/* Matches the name of the passed raw "header" against - the passed colon-separated "tick", and invalidates - the entry in tick. Entries can be prefixed for multi- or over-signing, - in which case do not invalidate. - - Returns OK for a match, or fail-code -*/ - -static int -header_name_match(const uschar * header, uschar * tick) -{ -const uschar * ticklist = tick; -int sep = ':'; -BOOL multisign; -uschar * hname, * p, * ele; -uschar * hcolon = Ustrchr(header, ':'); /* Get header name */ - -if (!hcolon) - return PDKIM_FAIL; /* This isn't a header */ - -/* if we had strncmpic() we wouldn't need this copy */ -hname = string_copyn(header, hcolon-header); - -while (p = US ticklist, ele = string_nextinlist(&ticklist, &sep, NULL, 0)) - { - switch (*ele) - { - case '=': case '+': multisign = TRUE; ele++; break; - default: multisign = FALSE; break; - } - - if (strcmpic(ele, hname) == 0) - { - if (!multisign) - *p = '_'; /* Invalidate this header name instance in tick-off list */ - return PDKIM_OK; - } - } -return PDKIM_FAIL; -} - - -/* -------------------------------------------------------------------------- */ -/* Performs "relaxed" canonicalization of a header. */ - -uschar * -pdkim_relax_header_n(const uschar * header, int len, BOOL append_crlf) -{ -BOOL past_field_name = FALSE; -BOOL seen_wsp = FALSE; -uschar * relaxed = store_get(len+3, GET_TAINTED); -uschar * q = relaxed; - -for (const uschar * p = header; p - header < len; p++) - { - uschar c = *p; - - if (c == '\r' || c == '\n') /* Ignore CR & LF */ - continue; - if (c == '\t' || c == ' ') - { - if (seen_wsp) - continue; - c = ' '; /* Turns WSP into SP */ - seen_wsp = TRUE; - } - else - if (!past_field_name && c == ':') - { - if (seen_wsp) q--; /* This removes WSP immediately before the colon */ - seen_wsp = TRUE; /* This removes WSP immediately after the colon */ - past_field_name = TRUE; - } - else - seen_wsp = FALSE; - - /* Lowercase header name */ - if (!past_field_name) c = tolower(c); - *q++ = c; - } - -if (q > relaxed && q[-1] == ' ') q--; /* Squash eventual trailing SP */ - -if (append_crlf) { *q++ = '\r'; *q++ = '\n'; } -*q = '\0'; -return relaxed; -} - - -uschar * -pdkim_relax_header(const uschar * header, BOOL append_crlf) -{ -return pdkim_relax_header_n(header, Ustrlen(header), append_crlf); -} - - -/* -------------------------------------------------------------------------- */ -#define PDKIM_QP_ERROR_DECODE -1 - -static const uschar * -pdkim_decode_qp_char(const uschar *qp_p, int *c) -{ -const uschar *initial_pos = qp_p; - -/* Advance one char */ -qp_p++; - -/* Check for two hex digits and decode them */ -if (isxdigit(*qp_p) && isxdigit(qp_p[1])) - { - /* Do hex conversion */ - *c = (isdigit(*qp_p) ? *qp_p - '0' : toupper(*qp_p) - 'A' + 10) << 4; - *c |= isdigit(qp_p[1]) ? qp_p[1] - '0' : toupper(qp_p[1]) - 'A' + 10; - return qp_p + 2; - } - -/* Illegal char here */ -*c = PDKIM_QP_ERROR_DECODE; -return initial_pos; -} - - -/* -------------------------------------------------------------------------- */ - -static uschar * -pdkim_decode_qp(const uschar * str) -{ -int nchar = 0; -uschar * q; -const uschar * p = str; -uschar * n = store_get(Ustrlen(str)+1, GET_TAINTED); - -*n = '\0'; -q = n; -while (*p) - { - if (*p == '=') - { - p = pdkim_decode_qp_char(p, &nchar); - if (nchar >= 0) - { - *q++ = nchar; - continue; - } - } - else - *q++ = *p; - p++; - } -*q = '\0'; -return n; -} - - -/* -------------------------------------------------------------------------- */ - -void -pdkim_decode_base64(const uschar * str, blob * b) -{ -int dlen = b64decode(str, &b->data, str); -if (dlen < 0) b->data = NULL; -b->len = dlen; -} - -uschar * -pdkim_encode_base64(blob * b) -{ -return b64encode(CUS b->data, b->len); -} - - -/* -------------------------------------------------------------------------- */ -#define PDKIM_HDR_LIMBO 0 -#define PDKIM_HDR_TAG 1 -#define PDKIM_HDR_VALUE 2 - -static pdkim_signature * -pdkim_parse_sig_header(pdkim_ctx * ctx, uschar * raw_hdr) -{ -pdkim_signature * sig = store_get(sizeof(pdkim_signature), GET_UNTAINTED); -uschar * q; -gstring * cur_tag = NULL, * cur_val = NULL; -BOOL past_hname = FALSE, in_b_val = FALSE; -int where = PDKIM_HDR_LIMBO; - -memset(sig, 0, sizeof(pdkim_signature)); -sig->bodylength = -1; - -/* Set so invalid/missing data error display is accurate */ -sig->version = 0; -sig->keytype = -1; -sig->hashtype = -1; - -q = sig->rawsig_no_b_val = store_get(Ustrlen(raw_hdr)+1, GET_TAINTED); - -for (uschar * p = raw_hdr; ; p++) - { - char c = *p; - - /* Ignore FWS */ - if (c == '\r' || c == '\n') - goto NEXT_CHAR; - - /* Fast-forward through header name */ - if (!past_hname) - { - if (c == ':') past_hname = TRUE; - goto NEXT_CHAR; - } - - if (where == PDKIM_HDR_LIMBO) - { - /* In limbo, just wait for a tag-char to appear */ - if (!(c >= 'a' && c <= 'z')) - goto NEXT_CHAR; - - where = PDKIM_HDR_TAG; - } - - if (where == PDKIM_HDR_TAG) - if (c == '=') - { - if (Ustrcmp(string_from_gstring(cur_tag), "b") == 0) - { - *q++ = '='; - in_b_val = TRUE; - } - where = PDKIM_HDR_VALUE; - goto NEXT_CHAR; - } - else if (!isspace(c)) - cur_tag = string_catn(cur_tag, p, 1); - - if (where == PDKIM_HDR_VALUE) - { - if (c == '\r' || c == '\n' || c == ' ' || c == '\t') - goto NEXT_CHAR; - - if (c == ';' || c == '\0') - { - /* We must have both tag and value, and tags must be one char except - for the possibility of "bh". */ - - if ( cur_tag && cur_val - && (cur_tag->ptr == 1 || *cur_tag->s == 'b') - ) - { - (void) string_from_gstring(cur_val); - pdkim_strtrim(cur_val); - - DEBUG(D_acl) debug_printf(" %s=%s\n", cur_tag->s, cur_val->s); - - switch (*cur_tag->s) - { - case 'b': /* sig-data or body-hash */ - switch (cur_tag->s[1]) - { - case '\0': pdkim_decode_base64(cur_val->s, &sig->sighash); break; - case 'h': if (cur_tag->ptr != 2) goto bad_tag; - pdkim_decode_base64(cur_val->s, &sig->bodyhash); - break; - default: goto bad_tag; - } - break; - case 'v': /* version */ - /* We only support version 1, and that is currently the - only version there is. */ - sig->version = - Ustrcmp(cur_val->s, PDKIM_SIGNATURE_VERSION) == 0 ? 1 : -1; - break; - case 'a': /* algorithm */ - { - const uschar * list = cur_val->s; - int sep = '-'; - uschar * elem; - - if ((elem = string_nextinlist(&list, &sep, NULL, 0))) - sig->keytype = pdkim_keyname_to_keytype(elem); - if ((elem = string_nextinlist(&list, &sep, NULL, 0))) - for (int i = 0; i < nelem(pdkim_hashes); i++) - if (Ustrcmp(elem, pdkim_hashes[i].dkim_hashname) == 0) - { sig->hashtype = i; break; } - } - break; - - case 'c': /* canonicalization */ - pdkim_cstring_to_canons(cur_val->s, 0, - &sig->canon_headers, &sig->canon_body); - break; - case 'q': /* Query method (for pubkey)*/ - for (int i = 0; pdkim_querymethods[i]; i++) - if (Ustrcmp(cur_val->s, pdkim_querymethods[i]) == 0) - { - sig->querymethod = i; /* we never actually use this */ - break; - } - break; - case 's': /* Selector */ - sig->selector = string_copy_from_gstring(cur_val); break; - case 'd': /* SDID */ - sig->domain = string_copy_from_gstring(cur_val); break; - case 'i': /* AUID */ - sig->identity = pdkim_decode_qp(cur_val->s); break; - case 't': /* Timestamp */ - sig->created = strtoul(CS cur_val->s, NULL, 10); break; - case 'x': /* Expiration */ - sig->expires = strtoul(CS cur_val->s, NULL, 10); break; - case 'l': /* Body length count */ - sig->bodylength = strtol(CS cur_val->s, NULL, 10); break; - case 'h': /* signed header fields */ - sig->headernames = string_copy_from_gstring(cur_val); break; - case 'z': /* Copied headfields */ - sig->copiedheaders = pdkim_decode_qp(cur_val->s); break; -/*XXX draft-ietf-dcrup-dkim-crypto-05 would need 'p' tag support -for rsafp signatures. But later discussion is dropping those. */ - default: - goto bad_tag; - } - } - else -bad_tag: DEBUG(D_acl) debug_printf(" Unknown tag encountered: %Y\n", cur_tag); - - cur_tag = cur_val = NULL; - in_b_val = FALSE; - where = PDKIM_HDR_LIMBO; - } - else - cur_val = string_catn(cur_val, p, 1); - } - -NEXT_CHAR: - if (c == '\0') - break; - - if (!in_b_val) - *q++ = c; - } - -if (sig->keytype < 0 || sig->hashtype < 0) /* Cannot verify this signature */ - return NULL; - -*q = '\0'; -/* Chomp raw header. The final newline must not be added to the signature. */ -while (--q > sig->rawsig_no_b_val && (*q == '\r' || *q == '\n')) - *q = '\0'; - -DEBUG(D_acl) - { - debug_printf( - "DKIM >> Raw signature w/o b= tag value >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); - debug_printf("%Z\n", US sig->rawsig_no_b_val); - debug_printf( - "DKIM >> Sig size: %4u bits\n", (unsigned) sig->sighash.len*8); - debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - } - -if (!pdkim_set_sig_bodyhash(ctx, sig)) - return NULL; - -return sig; -} - - -/* -------------------------------------------------------------------------- */ - -pdkim_pubkey * -pdkim_parse_pubkey_record(const uschar * raw_record) -{ -pdkim_pubkey * pub = store_get(sizeof(pdkim_pubkey), GET_TAINTED); - -memset(pub, 0, sizeof(pdkim_pubkey)); - -for (const uschar * ele = raw_record, * tspec, * end, * val; *ele; ele = end) - { - Uskip_whitespace(&ele); - end = Ustrchrnul(ele, ';'); - tspec = string_copyn(ele, end - ele); - if (*end) end++; /* skip the ; */ - - if ((val = Ustrchr(tspec, '='))) - { - int taglen = val++ - tspec; - - DEBUG(D_acl) debug_printf(" %.*s=%s\n", taglen, tspec, val); - while (taglen > 1 && isspace(tspec[taglen-1])) - taglen--; /* Ignore whitespace before = */ - Uskip_whitespace(&val); /* Ignore whitespace after = */ - if (isspace(val[ Ustrlen(val)-1 ])) - { /* Ignore whitespace after value */ - gstring * g = string_cat(NULL, val); - while (isspace(gstring_last_char(g))) - gstring_trim(g, 1); - val = string_from_gstring(g); - } - - if (taglen == 1) switch (tspec[0]) - { - case 'v': pub->version = val; break; - case 'h': pub->hashes = val; break; - case 'k': pub->keytype = val; break; - case 'g': pub->granularity = val; break; - case 'n': pub->notes = pdkim_decode_qp(val); break; - case 'p': pdkim_decode_base64(val, &pub->key); break; - case 's': pub->srvtype = val; break; - case 't': if (Ustrchr(val, 'y')) pub->testing = 1; - if (Ustrchr(val, 's')) pub->no_subdomaining = 1; - break; - default: goto bad_tag; - } - else -bad_tag: - DEBUG(D_acl) debug_printf(" Unknown tag encountered\n"); - } - } - -/* Set fallback defaults */ -if (!pub->version) - pub->version = string_copy(PDKIM_PUB_RECORD_VERSION); -else if (Ustrcmp(pub->version, PDKIM_PUB_RECORD_VERSION) != 0) - { - DEBUG(D_acl) debug_printf(" Bad v= field\n"); - return NULL; - } - -if (!pub->granularity) pub->granularity = US"*"; -if (!pub->keytype ) pub->keytype = US"rsa"; -if (!pub->srvtype ) pub->srvtype = US"*"; - -/* p= is required */ -if (pub->key.data) - return pub; - -DEBUG(D_acl) debug_printf(" Missing p= field\n"); -return NULL; -} - - -/* -------------------------------------------------------------------------- */ - -/* Update one bodyhash with some additional data. -If we have to relax the data for this sig, return our copy of it. */ - -static blob * -pdkim_update_ctx_bodyhash(pdkim_bodyhash * b, const blob * orig_data, blob * relaxed_data) -{ -const blob * canon_data = orig_data; -size_t left; - -/* Defaults to simple canon (no further treatment necessary) */ - -if (b->canon_method == PDKIM_CANON_RELAXED) - { - /* Relax the line if not done already */ - if (!relaxed_data) - { - BOOL seen_wsp = FALSE; - int q = 0; - - /* We want to be able to free this else we allocate - for the entire message which could be many MB. Since - we don't know what allocations the SHA routines might - do, not safe to use store_get()/store_reset(). */ - - relaxed_data = store_malloc(sizeof(blob) + orig_data->len+1); - relaxed_data->data = US (relaxed_data+1); - - for (const uschar * p = orig_data->data, * r = p + orig_data->len; p < r; p++) - { - char c = *p; - if (c == '\r') - { - if (q > 0 && relaxed_data->data[q-1] == ' ') - q--; - } - else if (c == '\t' || c == ' ') - { - c = ' '; /* Turns WSP into SP */ - if (seen_wsp) - continue; - seen_wsp = TRUE; - } - else - seen_wsp = FALSE; - relaxed_data->data[q++] = c; - } - relaxed_data->data[q] = '\0'; - relaxed_data->len = q; - } - canon_data = relaxed_data; - } - -/* Make sure we don't exceed the to-be-signed body length */ -left = canon_data->len; -if ( b->bodylength >= 0 - && left > (unsigned long)b->bodylength - b->signed_body_bytes - ) - left = (unsigned long)b->bodylength - b->signed_body_bytes; - -if (left > 0) - { - exim_sha_update(&b->body_hash_ctx, CUS canon_data->data, left); - b->signed_body_bytes += left; - DEBUG(D_acl) debug_printf("%.*Z\n", left, canon_data->data); - } - -return relaxed_data; -} - - -/* -------------------------------------------------------------------------- */ - -static void -pdkim_finish_bodyhash(pdkim_ctx * ctx) -{ -for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) /* Finish hashes */ - { - DEBUG(D_acl) debug_printf("DKIM: finish bodyhash %s/%s/%ld len %ld\n", - pdkim_hashes[b->hashtype].dkim_hashname, pdkim_canons[b->canon_method], - b->bodylength, b->signed_body_bytes); - exim_sha_finish(&b->body_hash_ctx, &b->bh); - } - -/* Traverse all signatures */ -for (pdkim_signature * sig = ctx->sig; sig; sig = sig->next) - { - pdkim_bodyhash * b = sig->calc_body_hash; - - DEBUG(D_acl) - { - debug_printf("DKIM [%s]%s Body bytes (%s) hashed: %lu\n" - "DKIM [%s]%s Body %s computed: ", - sig->domain, sig->selector, pdkim_canons[b->canon_method], b->signed_body_bytes, - sig->domain, sig->selector, pdkim_hashes[b->hashtype].dkim_hashname); - debug_printf("%.*H\n", b->bh.len, CUS b->bh.data); - } - - /* SIGNING -------------------------------------------------------------- */ - if (ctx->flags & PDKIM_MODE_SIGN) - { - /* If bodylength limit is set, and we have received less bytes - than the requested amount, effectively remove the limit tag. */ - if (b->signed_body_bytes < sig->bodylength) - sig->bodylength = -1; - } - - else - /* VERIFICATION --------------------------------------------------------- */ - /* Be careful that the header sig included a bodyash */ - - if (sig->bodyhash.data && sig->bodyhash.len == b->bh.len - && memcmp(b->bh.data, sig->bodyhash.data, b->bh.len) == 0) - { - DEBUG(D_acl) debug_printf("DKIM [%s] Body hash compared OK\n", sig->domain); - } - else - { - DEBUG(D_acl) - { - debug_printf("DKIM [%s] Body hash signature from headers: ", sig->domain); - debug_printf("%.*H\n", sig->bodyhash.len, sig->bodyhash.data); - debug_printf("DKIM [%s] Body hash did NOT verify\n", sig->domain); - } - sig->verify_status = PDKIM_VERIFY_FAIL; - sig->verify_ext_status = PDKIM_VERIFY_FAIL_BODY; - } - } -} - - - -static void -pdkim_body_complete(pdkim_ctx * ctx) -{ -/* In simple body mode, if any empty lines were buffered, -replace with one. rfc 4871 3.4.3 */ -/*XXX checking the signed-body-bytes is a gross hack; I think -it indicates that all linebreaks should be buffered, including -the one terminating a text line */ - -for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) - if ( b->canon_method == PDKIM_CANON_SIMPLE - && b->signed_body_bytes == 0 - && b->num_buffered_blanklines > 0 - ) - (void) pdkim_update_ctx_bodyhash(b, &lineending, NULL); - -ctx->flags |= PDKIM_SEEN_EOD; -ctx->linebuf_offset = 0; -} - - - -/* -------------------------------------------------------------------------- */ -/* Call from pdkim_feed below for processing complete body lines */ -/* NOTE: the line is not NUL-terminated; but we have a count */ - -static void -pdkim_bodyline_complete(pdkim_ctx * ctx) -{ -blob line = {.data = ctx->linebuf, .len = ctx->linebuf_offset}; -blob * rnl = NULL; -blob * rline = NULL; - -/* Ignore extra data if we've seen the end-of-data marker */ -if (ctx->flags & PDKIM_SEEN_EOD) goto all_skip; - -/* We've always got one extra byte to stuff a zero ... */ -ctx->linebuf[line.len] = '\0'; - -/* Terminate on EOD marker */ -if (ctx->flags & PDKIM_DOT_TERM) - { - if (memcmp(line.data, ".\r\n", 3) == 0) - { pdkim_body_complete(ctx); return; } - - /* Unstuff dots */ - if (memcmp(line.data, "..", 2) == 0) - { line.data++; line.len--; } - } - -/* Empty lines need to be buffered until we find a non-empty line */ -if (memcmp(line.data, "\r\n", 2) == 0) - { - for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) - b->num_buffered_blanklines++; - goto all_skip; - } - -/* Process line for each bodyhash separately */ -for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) - { - if (b->canon_method == PDKIM_CANON_RELAXED) - { - /* Lines with just spaces need to be buffered too */ - uschar * cp = line.data; - char c; - - while ((c = *cp)) - { - if (c == '\r' && cp[1] == '\n') break; - if (c != ' ' && c != '\t') goto hash_process; - cp++; - } - - b->num_buffered_blanklines++; - goto hash_skip; - } - -hash_process: - /* At this point, we have a non-empty line, so release the buffered ones. */ - - while (b->num_buffered_blanklines) - { - rnl = pdkim_update_ctx_bodyhash(b, &lineending, rnl); - b->num_buffered_blanklines--; - } - - rline = pdkim_update_ctx_bodyhash(b, &line, rline); -hash_skip: ; - } - -if (rnl) store_free(rnl); -if (rline) store_free(rline); - -all_skip: - -ctx->linebuf_offset = 0; -return; -} - - -/* -------------------------------------------------------------------------- */ -/* Callback from pdkim_feed below for processing complete headers */ -#define DKIM_SIGNATURE_HEADERNAME "DKIM-Signature:" - -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); - -#ifdef EXPERIMENTAL_ARC -/* Feed the header line to ARC processing */ -(void) arc_header_feed(ctx->cur_header, !(ctx->flags & PDKIM_MODE_SIGN)); -#endif - -if (++ctx->num_headers > PDKIM_MAX_HEADERS) goto BAIL; - -/* SIGNING -------------------------------------------------------------- */ -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); - -/* VERIFICATION ----------------------------------------------------------- */ -/* DKIM-Signature: headers are added to the verification list */ -else - { -#ifdef notdef - DEBUG(D_acl) debug_printf("DKIM >> raw hdr: %.*Z\n", - ctx->cur_head->ptr, CUS ctx->cur_header->s); -#endif - if (strncasecmp(CCS ctx->cur_header->s, - DKIM_SIGNATURE_HEADERNAME, - Ustrlen(DKIM_SIGNATURE_HEADERNAME)) == 0) - { - pdkim_signature * sig, * last_sig; - /* Create and chain new signature block. We could error-check for all - required tags here, but prefer to create the internal sig and expicitly - fail verification of it later. */ - - DEBUG(D_acl) debug_printf( - "DKIM >> Found sig, trying to parse >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); - - sig = pdkim_parse_sig_header(ctx, ctx->cur_header->s); - - if (!(last_sig = ctx->sig)) - ctx->sig = sig; - else - { - while (last_sig->next) last_sig = last_sig->next; - last_sig->next = sig; - } - - 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'; - return PDKIM_ERR_EXCESS_SIGS; - } - } - - /* all headers are stored for signature verification */ - ctx->headers = pdkim_prepend_stringlist(ctx->headers, ctx->cur_header->s); - } - -BAIL: -ctx->cur_header->s[ctx->cur_header->ptr = 0] = '\0'; /* leave buffer for reuse */ -return PDKIM_OK; -} - - - -/* -------------------------------------------------------------------------- */ -#define HEADER_BUFFER_FRAG_SIZE 256 - -DLLEXPORT int -pdkim_feed(pdkim_ctx * ctx, uschar * data, int len) -{ -/* Alternate EOD signal, used in non-dotstuffing mode */ -if (!data) - pdkim_body_complete(ctx); - -else for (int p = 0; p < len; p++) - { - uschar c = data[p]; - int rc; - - if (ctx->flags & PDKIM_PAST_HDRS) - { - if (c == '\n' && !(ctx->flags & PDKIM_SEEN_CR)) /* emulate the CR */ - { - ctx->linebuf[ctx->linebuf_offset++] = '\r'; - if (ctx->linebuf_offset == PDKIM_MAX_BODY_LINE_LEN-1) - return PDKIM_ERR_LONG_LINE; - } - - /* Processing body byte */ - ctx->linebuf[ctx->linebuf_offset++] = c; - if (c == '\r') - ctx->flags |= PDKIM_SEEN_CR; - else if (c == '\n') - { - ctx->flags &= ~PDKIM_SEEN_CR; - pdkim_bodyline_complete(ctx); - } - - if (ctx->linebuf_offset == PDKIM_MAX_BODY_LINE_LEN-1) - return PDKIM_ERR_LONG_LINE; - } - else - { - /* Processing header byte */ - if (c == '\r') - ctx->flags |= PDKIM_SEEN_CR; - else if (c == '\n') - { - if (!(ctx->flags & PDKIM_SEEN_CR)) /* emulate the CR */ - ctx->cur_header = string_catn(ctx->cur_header, CUS "\r", 1); - - if (ctx->flags & PDKIM_SEEN_LF) /* Seen last header line */ - { - if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK) - return rc; - - ctx->flags = (ctx->flags & ~(PDKIM_SEEN_LF|PDKIM_SEEN_CR)) | PDKIM_PAST_HDRS; - DEBUG(D_acl) debug_printf( - "DKIM >> Body data for hash, canonicalized >>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); - continue; - } - else - ctx->flags = (ctx->flags & ~PDKIM_SEEN_CR) | PDKIM_SEEN_LF; - } - else if (ctx->flags & PDKIM_SEEN_LF) - { - if (!(c == '\t' || c == ' ')) /* End of header */ - if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK) - return rc; - ctx->flags &= ~PDKIM_SEEN_LF; - } - - if (!ctx->cur_header || ctx->cur_header->ptr < PDKIM_MAX_HEADER_LEN) - ctx->cur_header = string_catn(ctx->cur_header, CUS &data[p], 1); - } - } -return PDKIM_OK; -} - - - -/* Extend a growing header with a continuation-linebreak */ -static gstring * -pdkim_hdr_cont(gstring * str, int * col) -{ -*col = 1; -return string_catn(str, US"\r\n\t", 3); -} - - - -/* - * RFC 5322 specifies that header line length SHOULD be no more than 78 - * pdkim_headcat - * - * Returns gstring (not nul-terminated) appending to one supplied - * - * col: this int holds and receives column number (octets since last '\n') - * str: partial string to append to - * pad: padding, split line or space after before or after eg: ";". - * Only the initial charater is used. - * intro: - must join to payload eg "h=", usually the tag name - * payload: eg base64 data - long data can be split arbitrarily. - * - * this code doesn't fold the header in some of the places that RFC4871 - * allows: As per RFC5322(2.2.3) it only folds before or after tag-value - * pairs and inside long values. it also always spaces or breaks after the - * "pad" - * - * No guarantees are made for output given out-of range input. like tag - * names longer than 78, or bogus col. Input is assumed to be free of line breaks. - */ - -static gstring * -pdkim_headcat(int * col, gstring * str, - const uschar * pad, const uschar * intro, const uschar * payload) -{ -int len, chomp, padded = 0; - -/* If we can fit at least the pad at the end of current line, do it now. -Otherwise, wrap if there is a pad. */ - -if (pad) - if (*col + 1 <= 78) - { - str = string_catn(str, pad, 1); - (*col)++; - pad = NULL; - padded = 1; - } - else - str = pdkim_hdr_cont(str, col); - -/* Special case: if the whole addition does not fit at the end of the current -line, but could fit on a new line, wrap to give it its full, dedicated line. */ - -len = (pad ? 2 : padded) - + (intro ? Ustrlen(intro) : 0) - + (payload ? Ustrlen(payload) : 0); -if (len <= 77 && *col+len > 78) - { - str = pdkim_hdr_cont(str, col); - padded = 0; - } - -/* Either we already dealt with the pad or we know there is room */ - -if (pad) - { - str = string_catn(str, pad, 1); - str = string_catn(str, US" ", 1); - *col += 2; - } -else if (padded && *col < 78) - { - str = string_catn(str, US" ", 1); - (*col)++; - } - -/* Call recursively with intro as payload: it gets the same, special treatment -(that is, not split if < 78). */ - -if (intro) - str = pdkim_headcat(col, str, NULL, NULL, intro); - -if (payload) - for (len = Ustrlen(payload); len; len -= chomp) - { - if (*col >= 78) - str = pdkim_hdr_cont(str, col); - chomp = *col+len > 78 ? 78 - *col : len; - str = string_catn(str, payload, chomp); - *col += chomp; - payload += chomp; - } - -return str; -} - - -/* -------------------------------------------------------------------------- */ - -/* Signing: create signature header -*/ -static uschar * -pdkim_create_header(pdkim_signature * sig, BOOL final) -{ -uschar * base64_bh; -uschar * base64_b; -int col = 0; -gstring * hdr; -gstring * canon_all; - -canon_all = string_cat (NULL, pdkim_canons[sig->canon_headers]); -canon_all = string_catn(canon_all, US"/", 1); -canon_all = string_cat (canon_all, pdkim_canons[sig->canon_body]); -(void) string_from_gstring(canon_all); - -hdr = string_cat(NULL, US"DKIM-Signature: v="PDKIM_SIGNATURE_VERSION); -col = hdr->ptr; - -/* Required and static bits */ -hdr = pdkim_headcat(&col, hdr, US";", US"a=", dkim_sig_to_a_tag(sig)); -hdr = pdkim_headcat(&col, hdr, US";", US"q=", pdkim_querymethods[sig->querymethod]); -hdr = pdkim_headcat(&col, hdr, US";", US"c=", canon_all->s); -hdr = pdkim_headcat(&col, hdr, US";", US"d=", sig->domain); -hdr = pdkim_headcat(&col, hdr, US";", US"s=", sig->selector); - -/* list of header names can be split between items. */ - { - uschar * n = string_copy(sig->headernames); - uschar * i = US"h="; - uschar * s = US";"; - - while (*n) - { - uschar * c = Ustrchr(n, ':'); - - if (c) *c ='\0'; - - if (!i) - hdr = pdkim_headcat(&col, hdr, NULL, NULL, US":"); - - hdr = pdkim_headcat(&col, hdr, s, i, n); - - if (!c) - break; - - n = c+1; - s = NULL; - i = NULL; - } - } - -base64_bh = pdkim_encode_base64(&sig->calc_body_hash->bh); -hdr = pdkim_headcat(&col, hdr, US";", US"bh=", base64_bh); - -/* Optional bits */ -if (sig->identity) - hdr = pdkim_headcat(&col, hdr, US";", US"i=", sig->identity); - -if (sig->created > 0) - { - uschar minibuf[21]; - - snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->created); - hdr = pdkim_headcat(&col, hdr, US";", US"t=", minibuf); -} - -if (sig->expires > 0) - { - uschar minibuf[21]; - - snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->expires); - hdr = pdkim_headcat(&col, hdr, US";", US"x=", minibuf); - } - -if (sig->bodylength >= 0) - { - uschar minibuf[21]; - - snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->bodylength); - hdr = pdkim_headcat(&col, hdr, US";", US"l=", minibuf); - } - -/* Preliminary or final version? */ -if (final) - { - base64_b = pdkim_encode_base64(&sig->sighash); - hdr = pdkim_headcat(&col, hdr, US";", US"b=", base64_b); - - /* add trailing semicolon: I'm not sure if this is actually needed */ - hdr = pdkim_headcat(&col, hdr, NULL, US";", US""); - } -else - { - /* To satisfy the rule "all surrounding whitespace [...] deleted" - ( RFC 6376 section 3.7 ) we ensure there is no whitespace here. Otherwise - the headcat routine could insert a linebreak which the relaxer would reduce - to a single space preceding the terminating semicolon, resulting in an - incorrect header-hash. */ - hdr = pdkim_headcat(&col, hdr, US";", US"b=;", US""); - } - -return string_from_gstring(hdr); -} - - -/* -------------------------------------------------------------------------- */ - -/* According to draft-ietf-dcrup-dkim-crypto-07 "keys are 256 bits" (referring -to DNS, hence the pubkey). Check for more than 32 bytes; if so assume the -alternate possible representation (still) being discussed: a -SubjectPublickeyInfo wrapped key - and drop all but the trailing 32-bytes (it -should be a DER, with exactly 12 leading bytes - but we could accept a BER also, -which could be any size). We still rely on the crypto library for checking for -undersize. - -When the RFC is published this should be re-addressed. */ - -static void -check_bare_ed25519_pubkey(pdkim_pubkey * p) -{ -int excess = p->key.len - 32; -if (excess > 0) - { - DEBUG(D_acl) - debug_printf("DKIM: unexpected pubkey len %lu\n", (unsigned long) p->key.len); - p->key.data += excess; p->key.len = 32; - } -} - - -static pdkim_pubkey * -pdkim_key_from_dns(pdkim_ctx * ctx, pdkim_signature * sig, ev_ctx * vctx, - const uschar ** errstr) -{ -uschar * dns_txt_name, * dns_txt_reply; -pdkim_pubkey * p; - -/* Fetch public key for signing domain, from DNS */ - -dns_txt_name = string_sprintf("%s._domainkey.%s.", sig->selector, sig->domain); - -if ( !(dns_txt_reply = ctx->dns_txt_callback(dns_txt_name)) - || dns_txt_reply[0] == '\0' - ) - { - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE; - return NULL; - } - -DEBUG(D_acl) - { - debug_printf( - "DKIM >> Parsing public key record >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" - " %s\n" - " Raw record: %Z\n", - dns_txt_name, - CUS dns_txt_reply); - } - -if ( !(p = pdkim_parse_pubkey_record(CUS dns_txt_reply)) - || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0) - ) - { - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD; - - DEBUG(D_acl) - { - if (p) - debug_printf(" Invalid public key service type '%s'\n", p->srvtype); - else - debug_printf(" Error while parsing public key record\n"); - debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - } - return NULL; - } - -DEBUG(D_acl) debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - -/* Import public key */ - -/* Normally we use the signature a= tag to tell us the pubkey format. -When signing under debug we do a test-import of the pubkey, and at that -time we do not have a signature so we must interpret the pubkey k= tag -instead. Assume writing on the sig is ok in that case. */ - -if (sig->keytype < 0) - if ((sig->keytype = pdkim_keyname_to_keytype(p->keytype)) < 0) - { - DEBUG(D_acl) debug_printf("verify_init: unhandled keytype %s\n", p->keytype); - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_IMPORT; - return NULL; - } - -if (sig->keytype == KEYTYPE_ED25519) - check_bare_ed25519_pubkey(p); - -if ((*errstr = exim_dkim_verify_init(&p->key, - sig->keytype == KEYTYPE_ED25519 ? KEYFMT_ED25519_BARE : KEYFMT_DER, - vctx, &sig->keybits))) - { - DEBUG(D_acl) debug_printf("verify_init: %s\n", *errstr); - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_IMPORT; - return NULL; - } - -vctx->keytype = sig->keytype; -return p; -} - - -/* -------------------------------------------------------------------------- */ -/* Sort and filter the sigs developed from the message */ - -static pdkim_signature * -sort_sig_methods(pdkim_signature * siglist) -{ -pdkim_signature * yield, ** ss; -const uschar * prefs; -uschar * ele; -int sep; - -if (!siglist) return NULL; - -/* first select in order of hashtypes */ -DEBUG(D_acl) debug_printf("DKIM: dkim_verify_hashes '%s'\n", dkim_verify_hashes); -for (prefs = dkim_verify_hashes, sep = 0, yield = NULL, ss = &yield; - ele = string_nextinlist(&prefs, &sep, NULL, 0); ) - { - int i = pdkim_hashname_to_hashtype(CUS ele, 0); - for (pdkim_signature * s = siglist, * next, ** prev = &siglist; s; - s = next) - { - next = s->next; - if (s->hashtype == i) - { *prev = next; s->next = NULL; *ss = s; ss = &s->next; } - else - prev = &s->next; - } - } - -/* then in order of keytypes */ -siglist = yield; -DEBUG(D_acl) debug_printf("DKIM: dkim_verify_keytypes '%s'\n", dkim_verify_keytypes); -for (prefs = dkim_verify_keytypes, sep = 0, yield = NULL, ss = &yield; - ele = string_nextinlist(&prefs, &sep, NULL, 0); ) - { - int i = pdkim_keyname_to_keytype(CUS ele); - for (pdkim_signature * s = siglist, * next, ** prev = &siglist; s; - s = next) - { - next = s->next; - if (s->keytype == i) - { *prev = next; s->next = NULL; *ss = s; ss = &s->next; } - else - prev = &s->next; - } - } - -DEBUG(D_acl) for (pdkim_signature * s = yield; s; s = s->next) - debug_printf(" retain d=%s s=%s a=%s\n", - s->domain, s->selector, dkim_sig_to_a_tag(s)); -return yield; -} - - -/* -------------------------------------------------------------------------- */ - -DLLEXPORT int -pdkim_feed_finish(pdkim_ctx * ctx, pdkim_signature ** return_signatures, - const uschar ** err) -{ -BOOL verify_pass = FALSE; - -/* Check if we must still flush a (partial) header. If that is the - case, the message has no body, and we must compute a body hash - out of '' */ -if (ctx->cur_header && ctx->cur_header->ptr > 0) - { - blob * rnl = NULL; - int rc; - - if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK) - return rc; - - for (pdkim_bodyhash * b = ctx->bodyhash; b; b = b->next) - rnl = pdkim_update_ctx_bodyhash(b, &lineending, rnl); - if (rnl) store_free(rnl); - } -else - DEBUG(D_acl) debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - -/* Build (and/or evaluate) body hash. Do this even if no DKIM sigs, in case we -have a hash to do for ARC. */ - -pdkim_finish_bodyhash(ctx); - -/* Sort and filter the recived signatures */ - -if (!(ctx->flags & PDKIM_MODE_SIGN)) - ctx->sig = sort_sig_methods(ctx->sig); - -if (!ctx->sig) - { - DEBUG(D_acl) debug_printf("DKIM: no signatures\n"); - *return_signatures = NULL; - return PDKIM_OK; - } - -for (pdkim_signature * sig = ctx->sig; sig; sig = sig->next) - { - hctx hhash_ctx; - uschar * sig_hdr = US""; - blob hhash; - gstring * hdata = NULL; - es_ctx sctx; - - if ( !(ctx->flags & PDKIM_MODE_SIGN) - && sig->verify_status == PDKIM_VERIFY_FAIL) - { - DEBUG(D_acl) - debug_printf("DKIM: [%s] abandoning this signature\n", sig->domain); - continue; - } - - /*XXX The hash of the headers is needed for GCrypt (for which we can do RSA - signing only, as it happens) and for either GnuTLS and OpenSSL when we are - signing with EC (specifically, Ed25519). The former is because the GCrypt - signing operation is pure (does not do its own hash) so we must hash. The - latter is because we (stupidly, but this is what the IETF draft is saying) - must hash with the declared hash method, then pass the result to the library - hash-and-sign routine (because that's all the libraries are providing. And - we're stuck with whatever that hidden hash method is, too). We may as well - do this hash incrementally. - We don't need the hash we're calculating here for the GnuTLS and OpenSSL - cases of RSA signing, since those library routines can do hash-and-sign. - - Some time in the future we could easily avoid doing the hash here for those - cases (which will be common for a long while. We could also change from - the current copy-all-the-headers-into-one-block, then call the hash-and-sign - implementation - to a proper incremental one. Unfortunately, GnuTLS just - cannot do incremental - either signing or verification. Unsure about GCrypt. - */ - - /*XXX The header hash is also used (so far) by the verify operation */ - - if (!exim_sha_init(&hhash_ctx, pdkim_hashes[sig->hashtype].exim_hashmethod)) - { - log_write(0, LOG_MAIN|LOG_PANIC, - "DKIM: hash setup error, possibly nonhandled hashtype"); - break; - } - - if (ctx->flags & PDKIM_MODE_SIGN) - DEBUG(D_acl) debug_printf( - "DKIM >> Headers to be signed: >>>>>>>>>>>>\n" - " %s\n", - sig->sign_headers); - - DEBUG(D_acl) debug_printf( - "DKIM >> Header data for hash, canonicalized (%-7s), in sequence >>\n", - pdkim_canons[sig->canon_headers]); - - - /* SIGNING ---------------------------------------------------------------- */ - /* When signing, walk through our header list and add them to the hash. As we - go, construct a list of the header's names to use for the h= parameter. - Then append to that list any remaining header names for which there was no - header to sign. */ - - if (ctx->flags & PDKIM_MODE_SIGN) - { - gstring * g = NULL; - const uschar * l; - uschar * s; - int sep = 0; - - /* Import private key, including the keytype which we need for building - the signature header */ - - if ((*err = exim_dkim_signing_init(CUS sig->privkey, &sctx))) - { - log_write(0, LOG_MAIN|LOG_PANIC, "signing_init: %s", *err); - return PDKIM_ERR_RSA_PRIVKEY; - } - sig->keytype = sctx.keytype; - - sig->headernames = NULL; /* Collected signed header names */ - for (pdkim_stringlist * p = sig->headers; p; p = p->next) - { - uschar * rh = p->value; - - if (header_name_match(rh, sig->sign_headers) == PDKIM_OK) - { - /* Collect header names (Note: colon presence is guaranteed here) */ - g = string_append_listele_n(g, ':', rh, Ustrchr(rh, ':') - rh); - - if (sig->canon_headers == PDKIM_CANON_RELAXED) - rh = pdkim_relax_header(rh, TRUE); /* cook header for relaxed canon */ - - /* Feed header to the hash algorithm */ - exim_sha_update_string(&hhash_ctx, CUS rh); - - /* Remember headers block for signing (when the library cannot do incremental) */ - /*XXX we could avoid doing this for all but the GnuTLS/RSA case */ - hdata = exim_dkim_data_append(hdata, rh); - - DEBUG(D_acl) debug_printf("%Z\n", rh); - } - } - - /* Any headers we wanted to sign but were not present must also be listed. - Ignore elements that have been ticked-off or are marked as never-oversign. */ - - l = sig->sign_headers; - while((s = string_nextinlist(&l, &sep, NULL, 0))) - { - if (*s == '+') /* skip oversigning marker */ - s++; - if (*s != '_' && *s != '=') - g = string_append_listele(g, ':', s); - } - sig->headernames = string_from_gstring(g); - - /* Create signature header with b= omitted */ - sig_hdr = pdkim_create_header(sig, FALSE); - } - - /* VERIFICATION ----------------------------------------------------------- */ - /* When verifying, walk through the header name list in the h= parameter and - add the headers to the hash in that order. */ - else - { - uschar * p = sig->headernames; - uschar * q; - - if (p) - { - /* clear tags */ - for (pdkim_stringlist * hdrs = ctx->headers; hdrs; hdrs = hdrs->next) - hdrs->tag = 0; - - p = string_copy(p); - while(1) - { - if ((q = Ustrchr(p, ':'))) - *q = '\0'; - - /*XXX walk the list of headers in same order as received. */ - for (pdkim_stringlist * hdrs = ctx->headers; hdrs; hdrs = hdrs->next) - if ( hdrs->tag == 0 - && strncasecmp(CCS hdrs->value, CCS p, Ustrlen(p)) == 0 - && (hdrs->value)[Ustrlen(p)] == ':' - ) - { - /* cook header for relaxed canon, or just copy it for simple */ - - uschar * rh = sig->canon_headers == PDKIM_CANON_RELAXED - ? pdkim_relax_header(hdrs->value, TRUE) - : string_copy(CUS hdrs->value); - - /* Feed header to the hash algorithm */ - exim_sha_update_string(&hhash_ctx, CUS rh); - - DEBUG(D_acl) debug_printf("%Z\n", rh); - hdrs->tag = 1; - break; - } - - if (!q) break; - p = q+1; - } - - sig_hdr = string_copy(sig->rawsig_no_b_val); - } - } - - DEBUG(D_acl) debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - - DEBUG(D_acl) - { - debug_printf( - "DKIM >> Signed DKIM-Signature header, pre-canonicalized >>>>>>>>>>>>>\n"); - debug_printf("%Z\n", CUS sig_hdr); - debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - } - - /* Relax header if necessary */ - if (sig->canon_headers == PDKIM_CANON_RELAXED) - sig_hdr = pdkim_relax_header(sig_hdr, FALSE); - - DEBUG(D_acl) - { - debug_printf("DKIM >> Signed DKIM-Signature header, canonicalized (%-7s) >>>>>>>\n", - pdkim_canons[sig->canon_headers]); - debug_printf("%Z\n", CUS sig_hdr); - debug_printf( - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - } - - /* Finalize header hash */ - exim_sha_update_string(&hhash_ctx, CUS sig_hdr); - exim_sha_finish(&hhash_ctx, &hhash); - - DEBUG(D_acl) - { - debug_printf("DKIM [%s] Header %s computed: ", - sig->domain, pdkim_hashes[sig->hashtype].dkim_hashname); - debug_printf("%.*H\n", hhash.len, hhash.data); - } - - /* Remember headers block for signing (when the signing library cannot do - incremental) */ - if (ctx->flags & PDKIM_MODE_SIGN) - hdata = exim_dkim_data_append(hdata, US sig_hdr); - - /* SIGNING ---------------------------------------------------------------- */ - if (ctx->flags & PDKIM_MODE_SIGN) - { - hashmethod hm = sig->keytype == KEYTYPE_ED25519 -#if defined(SIGN_OPENSSL) - ? HASH_NULL -#else - ? HASH_SHA2_512 -#endif - : pdkim_hashes[sig->hashtype].exim_hashmethod; - -#ifdef SIGN_HAVE_ED25519 - /* For GCrypt, and for EC, we pass the hash-of-headers to the signing - routine. For anything else we just pass the headers. */ - - if (sig->keytype != KEYTYPE_ED25519) -#endif - { - hhash.data = hdata->s; - hhash.len = hdata->ptr; - } - - if ((*err = exim_dkim_sign(&sctx, hm, &hhash, &sig->sighash))) - { - log_write(0, LOG_MAIN|LOG_PANIC, "signing: %s", *err); - return PDKIM_ERR_RSA_SIGNING; - } - - DEBUG(D_acl) - { - debug_printf( "DKIM [%s] b computed: ", sig->domain); - debug_printf("%.*H\n", sig->sighash.len, sig->sighash.data); - } - - sig->signature_header = pdkim_create_header(sig, TRUE); - } - - /* VERIFICATION ----------------------------------------------------------- */ - else - { - ev_ctx vctx; - hashmethod hm; - - /* Make sure we have all required signature tags */ - if (!( sig->domain && *sig->domain - && sig->selector && *sig->selector - && sig->headernames && *sig->headernames - && sig->bodyhash.data - && sig->sighash.data - && sig->keytype >= 0 - && sig->hashtype >= 0 - && sig->version - ) ) - { - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_SIGNATURE_ERROR; - - DEBUG(D_acl) debug_printf( - " Error in DKIM-Signature header: tags missing or invalid (%s)\n" - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n", - !(sig->domain && *sig->domain) ? "d=" - : !(sig->selector && *sig->selector) ? "s=" - : !(sig->headernames && *sig->headernames) ? "h=" - : !sig->bodyhash.data ? "bh=" - : !sig->sighash.data ? "b=" - : sig->keytype < 0 || sig->hashtype < 0 ? "a=" - : "v=" - ); - goto NEXT_VERIFY; - } - - /* Make sure sig uses supported DKIM version (only v1) */ - if (sig->version != 1) - { - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_DKIM_VERSION; - - DEBUG(D_acl) debug_printf( - " Error in DKIM-Signature header: unsupported DKIM version\n" - "DKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - goto NEXT_VERIFY; - } - - DEBUG(D_acl) - { - debug_printf( "DKIM [%s] b from mail: ", sig->domain); - debug_printf("%.*H\n", sig->sighash.len, sig->sighash.data); - } - - if (!(sig->pubkey = pdkim_key_from_dns(ctx, sig, &vctx, err))) - { - log_write(0, LOG_MAIN, "DKIM: %s%s %s%s [failed key import]", - sig->domain ? "d=" : "", sig->domain ? sig->domain : US"", - sig->selector ? "s=" : "", sig->selector ? sig->selector : US""); - goto NEXT_VERIFY; - } - - /* If the pubkey limits to a list of specific hashes, ignore sigs that - do not have the hash part of the sig algorithm matching */ - - if (sig->pubkey->hashes) - { - const uschar * list = sig->pubkey->hashes, * ele; - int sep = ':'; - while ((ele = string_nextinlist(&list, &sep, NULL, 0))) - if (Ustrcmp(ele, pdkim_hashes[sig->hashtype].dkim_hashname) == 0) break; - if (!ele) - { - DEBUG(D_acl) debug_printf("pubkey h=%s vs. sig a=%s_%s\n", - sig->pubkey->hashes, - pdkim_keytypes[sig->keytype], - pdkim_hashes[sig->hashtype].dkim_hashname); - sig->verify_status = PDKIM_VERIFY_FAIL; - sig->verify_ext_status = PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH; - goto NEXT_VERIFY; - } - } - - hm = sig->keytype == KEYTYPE_ED25519 -#if defined(SIGN_OPENSSL) - ? HASH_NULL -#else - ? HASH_SHA2_512 -#endif - : pdkim_hashes[sig->hashtype].exim_hashmethod; - - /* Check the signature */ - - if ((*err = exim_dkim_verify(&vctx, hm, &hhash, &sig->sighash))) - { - DEBUG(D_acl) debug_printf("headers verify: %s\n", *err); - sig->verify_status = PDKIM_VERIFY_FAIL; - sig->verify_ext_status = PDKIM_VERIFY_FAIL_MESSAGE; - goto NEXT_VERIFY; - } - if (*dkim_verify_min_keysizes) - { - unsigned minbits; - const uschar * ss = expand_getkeyed(US pdkim_keytypes[sig->keytype], - dkim_verify_min_keysizes); - if (ss && (minbits = atoi(CCS ss)) > sig->keybits) - { - DEBUG(D_acl) debug_printf("Key too short: Actual: %s %u Minima '%s'\n", - pdkim_keytypes[sig->keytype], sig->keybits, dkim_verify_min_keysizes); - sig->verify_status = PDKIM_VERIFY_FAIL; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE; - } - } - - - /* We have a winner! (if bodyhash was correct earlier) */ - if (sig->verify_status == PDKIM_VERIFY_NONE) - { - sig->verify_status = PDKIM_VERIFY_PASS; - verify_pass = TRUE; - /*XXX We used to "break" here if dkim_verify_minimal, but that didn't - stop the ACL being called. So move that test. Unfortunately, we - need to eval all the sigs here only to possibly ignore some later, - because we don't know what verify options might say. - Could we change to a later eval of the sig? - Both bits are called from receive_msg(). - Moving the test is also suboptimal for the case of no ACL (or no - signers to check!) so keep it for that case, but after debug output */ - } - -NEXT_VERIFY: - DEBUG(D_acl) - { - debug_printf("DKIM [%s] %s signature status: %s", - sig->domain, dkim_sig_to_a_tag(sig), - pdkim_verify_status_str(sig->verify_status)); - if (sig->verify_ext_status > 0) - debug_printf(" (%s)\n", - pdkim_verify_ext_status_str(sig->verify_ext_status)); - else - debug_printf("\n"); - } - - if ( verify_pass && dkim_verify_minimal - && !(acl_smtp_dkim && dkim_verify_signers && *dkim_verify_signers)) - break; - } - } - -/* If requested, set return pointer to signature(s) */ -if (return_signatures) - *return_signatures = ctx->sig; - -return ctx->flags & PDKIM_MODE_SIGN || verify_pass - ? PDKIM_OK : PDKIM_FAIL; -} - - -/* -------------------------------------------------------------------------- */ - -DLLEXPORT pdkim_ctx * -pdkim_init_verify(uschar * (*dns_txt_callback)(const uschar *), BOOL dot_stuffing) -{ -pdkim_ctx * ctx; - -ctx = store_get(sizeof(pdkim_ctx), GET_UNTAINTED); -memset(ctx, 0, sizeof(pdkim_ctx)); - -if (dot_stuffing) ctx->flags = PDKIM_DOT_TERM; -/* The line-buffer is for message data, hence tainted */ -ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN, GET_TAINTED); -ctx->dns_txt_callback = dns_txt_callback; -ctx->cur_header = string_get_tainted(36, GET_TAINTED); - -return ctx; -} - - -/* -------------------------------------------------------------------------- */ - -DLLEXPORT pdkim_signature * -pdkim_init_sign(pdkim_ctx * ctx, - uschar * domain, uschar * selector, uschar * privkey, - uschar * hashname, const uschar ** errstr) -{ -int hashtype; -pdkim_signature * sig; - -if (!domain || !selector || !privkey) - return NULL; - -/* Allocate & init one signature struct */ - -sig = store_get(sizeof(pdkim_signature), GET_UNTAINTED); -memset(sig, 0, sizeof(pdkim_signature)); - -sig->bodylength = -1; - -sig->domain = string_copy(US domain); -sig->selector = string_copy(US selector); -sig->privkey = string_copy(US privkey); -sig->keytype = -1; - -for (hashtype = 0; hashtype < nelem(pdkim_hashes); hashtype++) - if (Ustrcmp(hashname, pdkim_hashes[hashtype].dkim_hashname) == 0) - { sig->hashtype = hashtype; break; } -if (hashtype >= nelem(pdkim_hashes)) - { - log_write(0, LOG_MAIN|LOG_PANIC, - "DKIM: unrecognised hashname '%s'", hashname); - return NULL; - } - -DEBUG(D_acl) - { - pdkim_signature s = *sig; - ev_ctx vctx; - - debug_printf("DKIM (checking verify key)>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); - if (!pdkim_key_from_dns(ctx, &s, &vctx, errstr)) - debug_printf("WARNING: bad dkim key in dns\n"); - debug_printf("DKIM (finished checking verify key)<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - } -return sig; -} - - -/* -------------------------------------------------------------------------- */ - -DLLEXPORT void -pdkim_set_optional(pdkim_signature * sig, - char * sign_headers, - char * identity, - int canon_headers, - int canon_body, - long bodylength, - unsigned long created, - unsigned long expires) -{ -if (identity) - sig->identity = string_copy(US identity); - -sig->sign_headers = string_copy(sign_headers - ? US sign_headers : US PDKIM_DEFAULT_SIGN_HEADERS); - -sig->canon_headers = canon_headers; -sig->canon_body = canon_body; -sig->bodylength = bodylength; -sig->created = created; -sig->expires = expires; - -return; -} - - - -/* Set up a blob for calculating the bodyhash according to the -given needs. Use an existing one if possible, or create a new one. - -Return: hashblob pointer, or NULL on error -*/ -pdkim_bodyhash * -pdkim_set_bodyhash(pdkim_ctx * ctx, int hashtype, int canon_method, - long bodylength) -{ -pdkim_bodyhash * b; - -if (hashtype == -1 || canon_method == -1) return NULL; - -for (b = ctx->bodyhash; b; b = b->next) - if ( hashtype == b->hashtype - && canon_method == b->canon_method - && bodylength == b->bodylength) - { - DEBUG(D_receive) debug_printf("DKIM: using existing bodyhash %s/%s/%ld\n", - pdkim_hashes[hashtype].dkim_hashname, pdkim_canons[canon_method], bodylength); - return b; - } - -DEBUG(D_receive) debug_printf("DKIM: new bodyhash %s/%s/%ld\n", - pdkim_hashes[hashtype].dkim_hashname, pdkim_canons[canon_method], bodylength); -b = store_get(sizeof(pdkim_bodyhash), GET_UNTAINTED); -b->next = ctx->bodyhash; -b->hashtype = hashtype; -b->canon_method = canon_method; -b->bodylength = bodylength; -if (!exim_sha_init(&b->body_hash_ctx, /*XXX hash method: extend for sha512 */ - pdkim_hashes[hashtype].exim_hashmethod)) - { - DEBUG(D_acl) - debug_printf("DKIM: hash init error, possibly nonhandled hashtype\n"); - return NULL; - } -b->signed_body_bytes = 0; -b->num_buffered_blanklines = 0; -ctx->bodyhash = b; -return b; -} - - -/* Set up a blob for calculating the bodyhash according to the -needs of this signature. Use an existing one if possible, or -create a new one. - -Return: hashblob pointer, or NULL on error (only used as a boolean). -*/ -pdkim_bodyhash * -pdkim_set_sig_bodyhash(pdkim_ctx * ctx, pdkim_signature * sig) -{ -pdkim_bodyhash * b = pdkim_set_bodyhash(ctx, - sig->hashtype, sig->canon_body, sig->bodylength); -sig->calc_body_hash = b; -return b; -} - - -/* -------------------------------------------------------------------------- */ - - -void -pdkim_init_context(pdkim_ctx * ctx, BOOL dot_stuffed, - uschar * (*dns_txt_callback)(const uschar *)) -{ -memset(ctx, 0, sizeof(pdkim_ctx)); -ctx->flags = dot_stuffed ? PDKIM_MODE_SIGN | PDKIM_DOT_TERM : PDKIM_MODE_SIGN; -/* The line buffer is for message data, hence tainted */ -ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN, GET_TAINTED); -DEBUG(D_acl) ctx->dns_txt_callback = dns_txt_callback; -} - - -void -pdkim_init(void) -{ -exim_dkim_init(); -} - - - -#endif /*DISABLE_DKIM*/ diff --git a/src/src/pdkim/pdkim.h b/src/src/pdkim/pdkim.h deleted file mode 100644 index 5f91d3bc7..000000000 --- a/src/src/pdkim/pdkim.h +++ /dev/null @@ -1,368 +0,0 @@ -/* - * PDKIM - a RFC4871 (DKIM) implementation - * - * Copyright (C) 2009 - 2012 Tom Kistner - * Copyright (c) 2016 - 2020 Jeremy Harris - * SPDX-License-Identifier: GPL-2.0-or-later - * - * http://duncanthrax.net/pdkim/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ -#ifndef PDKIM_H -#define PDKIM_H - -#include "../blob.h" -#include "../hash.h" - -#define PDKIM_DEFAULT_SIGN_HEADERS "From:Sender:Reply-To:Subject:Date:"\ - "Message-ID:To:Cc:MIME-Version:Content-Type:"\ - "Content-Transfer-Encoding:Content-ID:"\ - "Content-Description:Resent-Date:Resent-From:"\ - "Resent-Sender:Resent-To:Resent-Cc:"\ - "Resent-Message-ID:In-Reply-To:References:"\ - "List-Id:List-Help:List-Unsubscribe:"\ - "List-Subscribe:List-Post:List-Owner:List-Archive" - -#define PDKIM_OVERSIGN_HEADERS "+From:+Sender:+Reply-To:+Subject:+Date:"\ - "+Message-ID:+To:+Cc:+MIME-Version:+Content-Type:"\ - "+Content-Transfer-Encoding:+Content-ID:"\ - "+Content-Description:+Resent-Date:+Resent-From:"\ - "+Resent-Sender:+Resent-To:+Resent-Cc:"\ - "+Resent-Message-ID:+In-Reply-To:+References:"\ - "+List-Id:+List-Help:+List-Unsubscribe:"\ - "+List-Subscribe:+List-Post:+List-Owner:+List-Archive" - -/* -------------------------------------------------------------------------- */ -/* Length of the preallocated buffer for the "answer" from the dns/txt - callback function. This should match the maximum RDLENGTH from DNS. */ -#define PDKIM_DNS_TXT_MAX_RECLEN (1 << 16) - -/* -------------------------------------------------------------------------- */ -/* Function success / error codes */ -#define PDKIM_OK 0 -#define PDKIM_FAIL -1 -#define PDKIM_ERR_RSA_PRIVKEY -101 -#define PDKIM_ERR_RSA_SIGNING -102 -#define PDKIM_ERR_LONG_LINE -103 -#define PDKIM_ERR_BUFFER_TOO_SMALL -104 -#define PDKIM_ERR_EXCESS_SIGS -105 -#define PDKIM_SIGN_PRIVKEY_WRAP -106 -#define PDKIM_SIGN_PRIVKEY_B64D -107 - -/* -------------------------------------------------------------------------- */ -/* Main/Extended verification status */ -#define PDKIM_VERIFY_NONE 0 -#define PDKIM_VERIFY_INVALID 1 -#define PDKIM_VERIFY_FAIL 2 -#define PDKIM_VERIFY_PASS 3 -#define PDKIM_VERIFY_POLICY BIT(31) - -#define PDKIM_VERIFY_FAIL_BODY 1 -#define PDKIM_VERIFY_FAIL_MESSAGE 2 -#define PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH 3 -#define PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE 4 -#define PDKIM_VERIFY_INVALID_BUFFER_SIZE 5 -#define PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD 6 -#define PDKIM_VERIFY_INVALID_PUBKEY_IMPORT 7 -#define PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE 8 -#define PDKIM_VERIFY_INVALID_SIGNATURE_ERROR 9 -#define PDKIM_VERIFY_INVALID_DKIM_VERSION 10 - -/* -------------------------------------------------------------------------- */ -/* Some parameter values */ -#define PDKIM_QUERYMETHOD_DNS_TXT 0 - -#define PDKIM_CANON_SIMPLE 0 -#define PDKIM_CANON_RELAXED 1 - -/* -------------------------------------------------------------------------- */ -/* Some required forward declarations, please ignore */ -typedef struct pdkim_stringlist pdkim_stringlist; -typedef struct pdkim_str pdkim_str; -typedef struct sha1_context sha1_context; -typedef struct sha2_context sha2_context; -#define HAVE_SHA1_CONTEXT -#define HAVE_SHA2_CONTEXT - -/* -------------------------------------------------------------------------- */ -/* Some concessions towards Redmond */ -#ifdef WINDOWS -#define snprintf _snprintf -#define strcasecmp _stricmp -#define strncasecmp _strnicmp -#define DLLEXPORT __declspec(dllexport) -#else -#define DLLEXPORT -#endif - - -/* -------------------------------------------------------------------------- */ -/* Public key as (usually) fetched from DNS */ -typedef struct pdkim_pubkey { - const uschar * version; /* v= */ - const uschar *granularity; /* g= */ - - const uschar * hashes; /* h= */ - const uschar * keytype; /* k= */ - const uschar * srvtype; /* s= */ - uschar *notes; /* n= */ - - blob key; /* p= */ - int testing; /* t=y */ - int no_subdomaining; /* t=s */ -} pdkim_pubkey; - -/* -------------------------------------------------------------------------- */ -/* Body-hash to be calculated */ -typedef struct pdkim_bodyhash { - struct pdkim_bodyhash * next; - int hashtype; - int canon_method; - long bodylength; - - hctx body_hash_ctx; - unsigned long signed_body_bytes; /* done so far */ - int num_buffered_blanklines; - - blob bh; /* completed hash */ -} pdkim_bodyhash; - -/* -------------------------------------------------------------------------- */ -/* Signature as it appears in a DKIM-Signature header */ -typedef struct pdkim_signature { - struct pdkim_signature * next; - - /* Bits stored in a DKIM signature header --------------------------- */ - - /* (v=) The version, as an integer. Currently, always "1" */ - int version; - - /* (a=) The signature algorithm. */ - int keytype; /* pdkim_keytypes index */ - unsigned keybits; /* size of the key */ - int hashtype; /* pdkim_hashes index */ - - /* (c=x/) Header canonicalization method. Either PDKIM_CANON_SIMPLE - or PDKIM_CANON_RELAXED */ - int canon_headers; - - /* (c=/x) Body canonicalization method. Either PDKIM_CANON_SIMPLE - or PDKIM_CANON_RELAXED */ - int canon_body; - - /* (q=) Query Method. Currently, only PDKIM_QUERYMETHOD_DNS_TXT - is specified */ - int querymethod; - - /* (s=) The selector string as given in the signature */ - uschar *selector; - - /* (d=) The domain as given in the signature */ - uschar *domain; - - /* (i=) The identity as given in the signature */ - uschar *identity; - - /* (t=) Timestamp of signature creation */ - unsigned long created; - - /* (x=) Timestamp of expiry of signature */ - unsigned long expires; - - /* (l=) Amount of hashed body bytes (after canonicalization). Default - is -1. Note: a value of 0 means that the body is unsigned! */ - long bodylength; - - /* (h=) Colon-separated list of header names that are included in the - signature */ - uschar *headernames; - - /* (z=) */ - uschar *copiedheaders; - - /* (b=) Raw signature data, along with its length in bytes */ - blob sighash; - - /* (bh=) Raw body hash data, along with its length in bytes */ - blob bodyhash; - - /* Folded DKIM-Signature: header. Signing only, NULL for verifying. - Ready for insertion into the message. Note: Folded using CRLFTB, - but final line terminator is NOT included. Note2: This buffer is - free()d when you call pdkim_free_ctx(). */ - uschar *signature_header; - - /* The main verification status. Verification only. One of: - - PDKIM_VERIFY_NONE Verification was not attempted. This status - should not appear. - - PDKIM_VERIFY_INVALID There was an error while trying to verify - the signature. A more precise description - is available in verify_ext_status. - - PDKIM_VERIFY_FAIL Verification failed because either the body - hash did not match, or the signature verification - failed. This means the message was modified. - Check verify_ext_status for the exact reason. - - PDKIM_VERIFY_PASS Verification succeeded. - */ - int verify_status; - - /* Extended verification status. Verification only. Depending on the value - of verify_status, it can contain: - - For verify_status == PDKIM_VERIFY_INVALID: - - PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE - Unable to retrieve a public key container. - - PDKIM_VERIFY_INVALID_BUFFER_SIZE - Either the DNS name constructed to retrieve the public key record - does not fit into PDKIM_DNS_TXT_MAX_NAMELEN bytes, or the retrieved - record is longer than PDKIM_DNS_TXT_MAX_RECLEN bytes. - - PDKIM_VERIFY_INVALID_PUBKEY_PARSING - (Syntax) error while parsing the retrieved public key record. - - - For verify_status == PDKIM_VERIFY_FAIL: - - PDKIM_VERIFY_FAIL_BODY - The calculated body hash does not match the advertised body hash - from the bh= tag of the signature. - - PDKIM_VERIFY_FAIL_MESSAGE - RSA verification of the signature (b= tag) failed. - */ - int verify_ext_status; - - /* Pointer to a public key record that was used to verify the signature. - See pdkim_pubkey declaration above for more information. - Caution: is NULL if signing or if no record was retrieved. */ - pdkim_pubkey *pubkey; - - /* Properties below this point are used internally only ------------- */ - - /* Per-signature helper variables ----------------------------------- */ - pdkim_bodyhash *calc_body_hash; /* hash to be / being calculated */ - - pdkim_stringlist *headers; /* Raw headers included in the sig */ - - /* Signing specific ------------------------------------------------- */ - uschar * privkey; /* Private key */ - uschar * sign_headers; /* To-be-signed header names */ - uschar * rawsig_no_b_val; /* Original signature header w/o b= tag value. */ -} pdkim_signature; - - -/* -------------------------------------------------------------------------- */ -/* Context to keep state between all operations. */ -typedef struct pdkim_ctx { - -#define PDKIM_MODE_SIGN BIT(0) /* if unset, mode==verify */ -#define PDKIM_DOT_TERM BIT(1) /* dot termination and unstuffing */ -#define PDKIM_SEEN_CR BIT(2) -#define PDKIM_SEEN_LF BIT(3) -#define PDKIM_PAST_HDRS BIT(4) -#define PDKIM_SEEN_EOD BIT(5) - unsigned flags; - - /* One (signing) or several chained (verification) signatures */ - pdkim_signature *sig; - - /* One (signing) or several chained (verification) bodyhashes */ - pdkim_bodyhash *bodyhash; - - /* Callback for dns/txt query method (verification only) */ - uschar * (*dns_txt_callback)(const uschar *); - - /* Coder's little helpers */ - gstring *cur_header; - uschar *linebuf; - int linebuf_offset; - int num_headers; - pdkim_stringlist *headers; /* Raw headers for verification */ -} pdkim_ctx; - - -/******************************************************************************/ - -typedef struct { - const uschar * dkim_hashname; - hashmethod exim_hashmethod; -} pdkim_hashtype; -extern const pdkim_hashtype pdkim_hashes[]; - -/******************************************************************************/ - - -/* -------------------------------------------------------------------------- */ -/* API functions. Please see the sample code in sample/test_sign.c and - sample/test_verify.c for documentation. -*/ - -#ifdef __cplusplus -extern "C" { -#endif - -void pdkim_init (void); - -void pdkim_init_context (pdkim_ctx *, BOOL, uschar * (*)(const uschar *)); - -DLLEXPORT -pdkim_signature *pdkim_init_sign (pdkim_ctx *, - uschar *, uschar *, uschar *, uschar *, - const uschar **); - -DLLEXPORT -pdkim_ctx *pdkim_init_verify (uschar * (*)(const uschar *), BOOL); - -DLLEXPORT -void pdkim_set_optional (pdkim_signature *, char *, char *,int, int, - long, - unsigned long, - unsigned long); - -int pdkim_hashname_to_hashtype(const uschar *, unsigned); -void pdkim_cstring_to_canons(const uschar *, unsigned, int *, int *); -pdkim_bodyhash *pdkim_set_bodyhash(pdkim_ctx *, int, int, long); -pdkim_bodyhash *pdkim_set_sig_bodyhash(pdkim_ctx *, pdkim_signature *); - -DLLEXPORT -int pdkim_feed (pdkim_ctx *, uschar *, int); -DLLEXPORT -int pdkim_feed_finish (pdkim_ctx *, pdkim_signature **, const uschar **); - -DLLEXPORT -void pdkim_free_ctx (pdkim_ctx *); - - -const uschar * pdkim_errstr(int); - -extern uschar * pdkim_encode_base64(blob *); -extern void pdkim_decode_base64(const uschar *, blob *); -extern pdkim_pubkey * pdkim_parse_pubkey_record(const uschar *); -extern uschar * pdkim_relax_header_n(const uschar *, int, BOOL); -extern uschar * pdkim_relax_header(const uschar *, BOOL); -extern uschar * dkim_sig_to_a_tag(const pdkim_signature *); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/src/pdkim/pdkim_hash.h b/src/src/pdkim/pdkim_hash.h deleted file mode 100644 index d56e3ce34..000000000 --- a/src/src/pdkim/pdkim_hash.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * PDKIM - a RFC4871 (DKIM) implementation - * - * Copyright (C) 1995 - 2018 Exim maintainers - * SPDX-License-Identifier: GPL-2.0-or-later - * - * Hash interface functions - */ - -#include "../exim.h" - -#if !defined(HASH_H) /* entire file */ -#define HASH_H - -#ifdef DISABLE_TLS -# error Must not DISABLE_TLS, for DKIM -#endif - -#include "crypt_ver.h" -#include "../blob.h" -#include "../hash.h" - -#ifdef SIGN_OPENSSL -# include -# include -# include -#elif defined(SIGN_GNUTLS) -# include -# include -#endif - -#if defined(SHA_OPENSSL) -# include "pdkim.h" -#elif defined(SHA_GCRYPT) -# include "pdkim.h" -#endif - -#endif -/* End of File */ diff --git a/src/src/pdkim/signing.c b/src/src/pdkim/signing.c deleted file mode 100644 index b564fb929..000000000 --- a/src/src/pdkim/signing.c +++ /dev/null @@ -1,904 +0,0 @@ -/* - * PDKIM - a RFC4871 (DKIM) implementation - * Copyright (c) The Exim Maintainers 1995 - 2024 - * SPDX-License-Identifier: GPL-2.0-or-later - * - * signing/verification interface - */ - -#include "../exim.h" -#include "crypt_ver.h" -#include "signing.h" - - -#ifdef MACRO_PREDEF -# include "../macro_predef.h" - -void -features_crypto(void) -{ -# ifdef SIGN_HAVE_ED25519 - builtin_macro_create(US"_CRYPTO_SIGN_ED25519"); -# endif -# ifdef EXIM_HAVE_SHA3 - builtin_macro_create(US"_CRYPTO_HASH_SHA3"); -# endif -} -#else - -#ifndef DISABLE_DKIM /* rest of file */ - -#ifdef DISABLE_TLS -# error Must no DISABLE_TLS, for DKIM -#endif - - -/******************************************************************************/ -#ifdef SIGN_GNUTLS -# define EXIM_GNUTLS_LIBRARY_LOG_LEVEL 3 - -# ifndef GNUTLS_VERIFY_ALLOW_BROKEN -# define GNUTLS_VERIFY_ALLOW_BROKEN 0 -# endif - - -/* Logging function which can be registered with - * gnutls_global_set_log_function() - * gnutls_global_set_log_level() 0..9 - */ -#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 -static void -exim_gnutls_logger_cb(int level, const char *message) -{ -size_t len = strlen(message); -if (len < 1) - { - DEBUG(D_tls) debug_printf("GnuTLS<%d> empty debug message\n", level); - return; - } -DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s%s", level, message, - message[len-1] == '\n' ? "" : "\n"); -} -#endif - - - -void -exim_dkim_init(void) -{ -#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 -DEBUG(D_tls) - { - gnutls_global_set_log_function(exim_gnutls_logger_cb); - /* arbitrarily chosen level; bump upto 9 for more */ - gnutls_global_set_log_level(EXIM_GNUTLS_LIBRARY_LOG_LEVEL); - } -#endif -} - - -/* accumulate data (gnutls-only). String to be appended must be nul-terminated. */ -gstring * -exim_dkim_data_append(gstring * g, uschar * s) -{ -return string_cat(g, s); -} - - - -/* import private key from PEM string in memory. -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_signing_init(const uschar * privkey_pem, es_ctx * sign_ctx) -{ -gnutls_datum_t k = { .data = (void *)privkey_pem, .size = Ustrlen(privkey_pem) }; -gnutls_x509_privkey_t x509_key; -const uschar * where; -int rc; - -if ( (where = US"internal init", rc = gnutls_x509_privkey_init(&x509_key)) - || (rc = gnutls_privkey_init(&sign_ctx->key)) - || (where = US"privkey PEM-block import", - rc = gnutls_x509_privkey_import(x509_key, &k, GNUTLS_X509_FMT_PEM)) - || (where = US"internal privkey transfer", - rc = gnutls_privkey_import_x509(sign_ctx->key, x509_key, 0)) - ) - return string_sprintf("%s: %s", where, gnutls_strerror(rc)); - -switch (rc = gnutls_privkey_get_pk_algorithm(sign_ctx->key, NULL)) - { - case GNUTLS_PK_RSA: sign_ctx->keytype = KEYTYPE_RSA; break; -#ifdef SIGN_HAVE_ED25519 - case GNUTLS_PK_EDDSA_ED25519: sign_ctx->keytype = KEYTYPE_ED25519; break; -#endif - default: return rc < 0 - ? CUS gnutls_strerror(rc) - : string_sprintf("Unhandled key type: %d '%s'", rc, gnutls_pk_get_name(rc)); - } - -return NULL; -} - - - -/* allocate mem for signature (when signing) */ -/* hash & sign data. No way to do incremental. - -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_sign(es_ctx * sign_ctx, hashmethod hash, blob * data, blob * sig) -{ -gnutls_datum_t k_data = { .data = data->data, .size = data->len }; -gnutls_digest_algorithm_t dig; -gnutls_datum_t k_sig; -int rc; - -switch (hash) - { - case HASH_SHA1: dig = GNUTLS_DIG_SHA1; break; - case HASH_SHA2_256: dig = GNUTLS_DIG_SHA256; break; - case HASH_SHA2_512: dig = GNUTLS_DIG_SHA512; break; - default: return US"nonhandled hash type"; - } - -if ((rc = gnutls_privkey_sign_data(sign_ctx->key, dig, 0, &k_data, &k_sig))) - return CUS gnutls_strerror(rc); - -/* Don't care about deinit for the key; shortlived process */ - -sig->data = k_sig.data; -sig->len = k_sig.size; -return NULL; -} - - - -/* import public key (from blob in memory) -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_verify_init(blob * pubkey, keyformat fmt, ev_ctx * verify_ctx, - unsigned * bits) -{ -gnutls_datum_t k; -int rc; -const uschar * ret = NULL; - -gnutls_pubkey_init(&verify_ctx->key); -k.data = pubkey->data; -k.size = pubkey->len; - -switch(fmt) - { - case KEYFMT_DER: - if ((rc = gnutls_pubkey_import(verify_ctx->key, &k, GNUTLS_X509_FMT_DER))) - ret = US gnutls_strerror(rc); - break; -#ifdef SIGN_HAVE_ED25519 - case KEYFMT_ED25519_BARE: - if ((rc = gnutls_pubkey_import_ecc_raw(verify_ctx->key, - GNUTLS_ECC_CURVE_ED25519, &k, NULL))) - ret = US gnutls_strerror(rc); - break; -#endif - default: - ret = US"pubkey format not handled"; - break; - } -if (!ret && bits) gnutls_pubkey_get_pk_algorithm(verify_ctx->key, bits); -return ret; -} - - -/* verify signature (of hash if RSA sig, of data if EC sig. No way to do incremental) -(given pubkey & alleged sig) -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_verify(ev_ctx * verify_ctx, hashmethod hash, blob * data_hash, blob * sig) -{ -gnutls_datum_t k = { .data = data_hash->data, .size = data_hash->len }; -gnutls_datum_t s = { .data = sig->data, .size = sig->len }; -int rc; -const uschar * ret = NULL; - -#ifdef SIGN_HAVE_ED25519 -if (verify_ctx->keytype == KEYTYPE_ED25519) - { - if ((rc = gnutls_pubkey_verify_data2(verify_ctx->key, - GNUTLS_SIGN_EDDSA_ED25519, 0, &k, &s)) < 0) - ret = US gnutls_strerror(rc); - } -else -#endif - { - gnutls_sign_algorithm_t algo; - switch (hash) - { - case HASH_SHA1: algo = GNUTLS_SIGN_RSA_SHA1; break; - case HASH_SHA2_256: algo = GNUTLS_SIGN_RSA_SHA256; break; - case HASH_SHA2_512: algo = GNUTLS_SIGN_RSA_SHA512; break; - default: return US"nonhandled hash type"; - } - - if ((rc = gnutls_pubkey_verify_hash2(verify_ctx->key, algo, - GNUTLS_VERIFY_ALLOW_BROKEN, &k, &s)) < 0) - ret = US gnutls_strerror(rc); - } - -gnutls_pubkey_deinit(verify_ctx->key); -return ret; -} - - - - -#elif defined(SIGN_GCRYPT) -/******************************************************************************/ -/* This variant is used under pre-3.0.0 GnuTLS. Only rsa-sha1 and rsa-sha256 */ - - -/* Internal service routine: -Read and move past an asn.1 header, checking class & tag, -optionally returning the data-length */ - -static int -as_tag(blob * der, uschar req_cls, long req_tag, long * alen) -{ -int rc; -uschar tag_class; -int taglen; -long tag, len; - -debug_printf_indent("as_tag: %02x %02x %02x %02x\n", - der->data[0], der->data[1], der->data[2], der->data[3]); - -if ((rc = asn1_get_tag_der(der->data++, der->len--, &tag_class, &taglen, &tag)) - != ASN1_SUCCESS) - return rc; - -if (tag_class != req_cls || tag != req_tag) return ASN1_ELEMENT_NOT_FOUND; - -if ((len = asn1_get_length_der(der->data, der->len, &taglen)) < 0) - return ASN1_DER_ERROR; -if (alen) *alen = len; - -/* debug_printf_indent("as_tag: tlen %d dlen %d\n", taglen, (int)len); */ - -der->data += taglen; -der->len -= taglen; -return rc; -} - -/* Internal service routine: -Read and move over an asn.1 integer, setting an MPI to the value -*/ - -static uschar * -as_mpi(blob * der, gcry_mpi_t * mpi) -{ -long alen; -int rc; -gcry_error_t gerr; - -debug_printf_indent("%s\n", __FUNCTION__); - -/* integer; move past the header */ -if ((rc = as_tag(der, 0, ASN1_TAG_INTEGER, &alen)) != ASN1_SUCCESS) - return US asn1_strerror(rc); - -/* read to an MPI */ -if ((gerr = gcry_mpi_scan(mpi, GCRYMPI_FMT_STD, der->data, alen, NULL))) - return US gcry_strerror(gerr); - -/* move over the data */ -der->data += alen; der->len -= alen; -return NULL; -} - - - -void -exim_dkim_init(void) -{ -/* Version check should be the very first call because it -makes sure that important subsystems are initialized. */ -if (!gcry_check_version (GCRYPT_VERSION)) - { - fputs ("libgcrypt version mismatch\n", stderr); - exim_exit(2); - } - -/* We don't want to see any warnings, e.g. because we have not yet -parsed program options which might be used to suppress such -warnings. */ -gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN); - -/* ... If required, other initialization goes here. Note that the -process might still be running with increased privileges and that -the secure memory has not been initialized. */ - -/* Allocate a pool of 16k secure memory. This make the secure memory -available and also drops privileges where needed. */ -gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0); - -/* It is now okay to let Libgcrypt complain when there was/is -a problem with the secure memory. */ -gcry_control (GCRYCTL_RESUME_SECMEM_WARN); - -/* ... If required, other initialization goes here. */ - -/* Tell Libgcrypt that initialization has completed. */ -gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); - -return; -} - - - - -/* Accumulate data (gnutls-only). -String to be appended must be nul-terminated. */ - -gstring * -exim_dkim_data_append(gstring * g, uschar * s) -{ -return g; /*dummy*/ -} - - - -/* import private key from PEM string in memory. -Only handles RSA keys. -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_signing_init(const uschar * privkey_pem, es_ctx * sign_ctx) -{ -uschar * s1, * s2; -blob der; -long alen; -int rc; - -/*XXX will need extension to _spot_ as well as handle a -non-RSA key? I think... -So... this is not a PrivateKeyInfo - which would have a field -identifying the keytype - PrivateKeyAlgorithmIdentifier - -but a plain RSAPrivateKey (wrapped in PEM-headers. Can we -use those as a type tag? What forms are there? "BEGIN EC PRIVATE KEY" (cf. ec(1ssl)) - -How does OpenSSL PEM_read_bio_PrivateKey() deal with it? -gnutls_x509_privkey_import() ? -*/ - -/* - * RSAPrivateKey ::= SEQUENCE - * version Version, - * modulus INTEGER, -- n - * publicExponent INTEGER, -- e - * privateExponent INTEGER, -- d - * prime1 INTEGER, -- p - * prime2 INTEGER, -- q - * exponent1 INTEGER, -- d mod (p-1) - * exponent2 INTEGER, -- d mod (q-1) - * coefficient INTEGER, -- (inverse of q) mod p - * otherPrimeInfos OtherPrimeInfos OPTIONAL - - * ECPrivateKey ::= SEQUENCE { - * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), - * privateKey OCTET STRING, - * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, - * publicKey [1] BIT STRING OPTIONAL - * } - * Hmm, only 1 useful item, and not even an integer? Wonder how we might use it... - -- actually, gnutls_x509_privkey_import() appears to require a curve name parameter - value for that is an OID? a local-only integer (it's an enum in GnuTLS)? - - -Useful cmds: - ssh-keygen -t ecdsa -f foo.privkey - ssh-keygen -t ecdsa -b384 -f foo.privkey - ssh-keygen -t ecdsa -b521 -f foo.privkey - ssh-keygen -t ed25519 -f foo.privkey - - < foo openssl pkcs8 -in /dev/stdin -inform PEM -nocrypt -topk8 -outform DER | od -x - - openssl asn1parse -in foo -inform PEM -dump - openssl asn1parse -in foo -inform PEM -dump -stroffset 24 (??) -(not good for ed25519) - - */ - -if ( !(s1 = Ustrstr(CS privkey_pem, "-----BEGIN RSA PRIVATE KEY-----")) - || !(s2 = Ustrstr(CS (s1+=31), "-----END RSA PRIVATE KEY-----" )) - ) - return US"Bad PEM wrapper"; - -*s2 = '\0'; - -if ((rc = b64decode(s1, &der.data, s1) < 0)) - return US"Bad PEM-DER b64 decode"; -der.len = rc; - -/* untangle asn.1 */ - -/* sequence; just move past the header */ -if ((rc = as_tag(&der, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, NULL)) - != ASN1_SUCCESS) goto asn_err; - -/* integer version; move past the header, check is zero */ -if ((rc = as_tag(&der, 0, ASN1_TAG_INTEGER, &alen)) != ASN1_SUCCESS) - goto asn_err; -if (alen != 1 || *der.data != 0) - return US"Bad version number"; -der.data++; der.len--; - -if ( (s1 = as_mpi(&der, &sign_ctx->n)) - || (s1 = as_mpi(&der, &sign_ctx->e)) - || (s1 = as_mpi(&der, &sign_ctx->d)) - || (s1 = as_mpi(&der, &sign_ctx->p)) - || (s1 = as_mpi(&der, &sign_ctx->q)) - || (s1 = as_mpi(&der, &sign_ctx->dp)) - || (s1 = as_mpi(&der, &sign_ctx->dq)) - || (s1 = as_mpi(&der, &sign_ctx->qp)) - ) - return s1; - -#ifdef extreme_debug -DEBUG(D_acl) debug_printf_indent("rsa_signing_init:\n"); - { - uschar * s; - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->n); - debug_printf_indent(" N : %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->e); - debug_printf_indent(" E : %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->d); - debug_printf_indent(" D : %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->p); - debug_printf_indent(" P : %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->q); - debug_printf_indent(" Q : %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->dp); - debug_printf_indent(" DP: %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->dq); - debug_printf_indent(" DQ: %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, sign_ctx->qp); - debug_printf_indent(" QP: %s\n", s); - } -#endif - -sign_ctx->keytype = KEYTYPE_RSA; -return NULL; - -asn_err: return US asn1_strerror(rc); -} - - - -/* allocate mem for signature (when signing) */ -/* sign already-hashed data. - -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_sign(es_ctx * sign_ctx, hashmethod hash, blob * data, blob * sig) -{ -char * sexp_hash; -gcry_sexp_t s_hash = NULL, s_key = NULL, s_sig = NULL; -gcry_mpi_t m_sig; -uschar * errstr; -gcry_error_t gerr; - -/*XXX will need extension for hash types (though, possibly, should -be re-specced to not rehash but take an already-hashed value? Actually -current impl looks WRONG - it _is_ given a hash so should not be -re-hashing. Has this been tested? - -Will need extension for non-RSA sugning algos. */ - -switch (hash) - { - case HASH_SHA1: sexp_hash = "(data(flags pkcs1)(hash sha1 %b))"; break; - case HASH_SHA2_256: sexp_hash = "(data(flags pkcs1)(hash sha256 %b))"; break; - default: return US"nonhandled hash type"; - } - -#define SIGSPACE 128 -sig->data = store_get(SIGSPACE, GET_UNTAINTED); - -if (gcry_mpi_cmp (sign_ctx->p, sign_ctx->q) > 0) - { - gcry_mpi_swap (sign_ctx->p, sign_ctx->q); - gcry_mpi_invm (sign_ctx->qp, sign_ctx->p, sign_ctx->q); - } - -if ( (gerr = gcry_sexp_build (&s_key, NULL, - "(private-key (rsa (n%m)(e%m)(d%m)(p%m)(q%m)(u%m)))", - sign_ctx->n, sign_ctx->e, - sign_ctx->d, sign_ctx->p, - sign_ctx->q, sign_ctx->qp)) - || (gerr = gcry_sexp_build (&s_hash, NULL, sexp_hash, - (int) data->len, CS data->data)) - || (gerr = gcry_pk_sign (&s_sig, s_hash, s_key)) - ) - return US gcry_strerror(gerr); - -/* gcry_sexp_dump(s_sig); */ - -if ( !(s_sig = gcry_sexp_find_token(s_sig, "s", 0)) - ) - return US"no sig result"; - -m_sig = gcry_sexp_nth_mpi(s_sig, 1, GCRYMPI_FMT_USG); - -#ifdef extreme_debug -DEBUG(D_acl) - { - uschar * s; - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, m_sig); - debug_printf_indent(" SG: %s\n", s); - } -#endif - -gerr = gcry_mpi_print(GCRYMPI_FMT_USG, sig->data, SIGSPACE, &sig->len, m_sig); -if (gerr) - { - debug_printf_indent("signature conversion from MPI to buffer failed\n"); - return US gcry_strerror(gerr); - } -#undef SIGSPACE - -return NULL; -} - - -/* import public key (from blob in memory) -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_verify_init(blob * pubkey, keyformat fmt, ev_ctx * verify_ctx, - unsigned * bits) -{ -/* -in code sequence per b81207d2bfa92 rsa_parse_public_key() and asn1_get_mpi() -*/ -uschar tag_class; -int taglen; -long alen; -unsigned nbits; -int rc; -uschar * errstr; -gcry_error_t gerr; -uschar * stage = US"S1"; - -if (fmt != KEYFMT_DER) return US"pubkey format not handled"; - -/* -sequence - sequence - OBJECT:rsaEncryption - NULL - BIT STRING:RSAPublicKey - sequence - INTEGER:Public modulus - INTEGER:Public exponent - -openssl rsa -in aux-fixed/dkim/dkim.private -pubout -outform DER | od -t x1 | head; -openssl rsa -in aux-fixed/dkim/dkim.private -pubout | openssl asn1parse -dump; -openssl rsa -in aux-fixed/dkim/dkim.private -pubout | openssl asn1parse -dump -offset 22; -*/ - -/* sequence; just move past the header */ -if ((rc = as_tag(pubkey, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, NULL)) - != ASN1_SUCCESS) goto asn_err; - -/* sequence; skip the entire thing */ -DEBUG(D_acl) stage = US"S2"; -if ((rc = as_tag(pubkey, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, &alen)) - != ASN1_SUCCESS) goto asn_err; -pubkey->data += alen; pubkey->len -= alen; - - -/* bitstring: limit range to size of bitstring; -move over header + content wrapper */ -DEBUG(D_acl) stage = US"BS"; -if ((rc = as_tag(pubkey, 0, ASN1_TAG_BIT_STRING, &alen)) != ASN1_SUCCESS) - goto asn_err; -pubkey->len = alen; -pubkey->data++; pubkey->len--; - -/* sequence; just move past the header */ -DEBUG(D_acl) stage = US"S3"; -if ((rc = as_tag(pubkey, ASN1_CLASS_STRUCTURED, ASN1_TAG_SEQUENCE, NULL)) - != ASN1_SUCCESS) goto asn_err; - -/* read two integers */ -DEBUG(D_acl) stage = US"MPI"; -nbits = pubkey->len; -if ((errstr = as_mpi(pubkey, &verify_ctx->n))) return errstr; -nbits = (nbits - pubkey->len) * 8; -if ((errstr = as_mpi(pubkey, &verify_ctx->e))) return errstr; - -#ifdef extreme_debug -DEBUG(D_acl) debug_printf_indent("rsa_verify_init:\n"); - { - uschar * s; - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, verify_ctx->n); - debug_printf_indent(" N : %s\n", s); - gcry_mpi_aprint (GCRYMPI_FMT_HEX, &s, NULL, verify_ctx->e); - debug_printf_indent(" E : %s\n", s); - } - -#endif -if (bits) *bits = nbits; -return NULL; - -asn_err: -DEBUG(D_acl) return string_sprintf("%s: %s", stage, asn1_strerror(rc)); - return US asn1_strerror(rc); -} - - -/* verify signature (of hash) -XXX though we appear to be doing a hash, too! -(given pubkey & alleged sig) -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_verify(ev_ctx * verify_ctx, hashmethod hash, blob * data_hash, blob * sig) -{ -/* -cf. libgnutls 2.8.5 _wrap_gcry_pk_verify() -*/ -char * sexp_hash; -gcry_mpi_t m_sig; -gcry_sexp_t s_sig = NULL, s_hash = NULL, s_pkey = NULL; -gcry_error_t gerr; -uschar * stage; - -/*XXX needs extension for SHA512 */ -switch (hash) - { - case HASH_SHA1: sexp_hash = "(data(flags pkcs1)(hash sha1 %b))"; break; - case HASH_SHA2_256: sexp_hash = "(data(flags pkcs1)(hash sha256 %b))"; break; - default: return US"nonhandled hash type"; - } - -if ( (stage = US"pkey sexp build", - gerr = gcry_sexp_build (&s_pkey, NULL, "(public-key(rsa(n%m)(e%m)))", - verify_ctx->n, verify_ctx->e)) - || (stage = US"data sexp build", - gerr = gcry_sexp_build (&s_hash, NULL, sexp_hash, - (int) data_hash->len, CS data_hash->data)) - || (stage = US"sig mpi scan", - gerr = gcry_mpi_scan(&m_sig, GCRYMPI_FMT_USG, sig->data, sig->len, NULL)) - || (stage = US"sig sexp build", - gerr = gcry_sexp_build (&s_sig, NULL, "(sig-val(rsa(s%m)))", m_sig)) - || (stage = US"verify", - gerr = gcry_pk_verify (s_sig, s_hash, s_pkey)) - ) - { - DEBUG(D_acl) debug_printf_indent("verify: error in stage '%s'\n", stage); - return US gcry_strerror(gerr); - } - -if (s_sig) gcry_sexp_release (s_sig); -if (s_hash) gcry_sexp_release (s_hash); -if (s_pkey) gcry_sexp_release (s_pkey); -gcry_mpi_release (m_sig); -gcry_mpi_release (verify_ctx->n); -gcry_mpi_release (verify_ctx->e); - -return NULL; -} - - - - -#elif defined(SIGN_OPENSSL) -/******************************************************************************/ - -void -exim_dkim_init(void) -{ -ERR_load_crypto_strings(); -} - - -/* accumulate data (was gnutls-only but now needed for OpenSSL non-EC too -because now using hash-and-sign interface) */ -gstring * -exim_dkim_data_append(gstring * g, uschar * s) -{ -return string_cat(g, s); -} - - -/* import private key from PEM string in memory. -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_signing_init(const uschar * privkey_pem, es_ctx * sign_ctx) -{ -BIO * bp = BIO_new_mem_buf((void *)privkey_pem, -1); - -if (!(sign_ctx->key = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL))) - return string_sprintf("privkey PEM-block import: %s", - ERR_error_string(ERR_get_error(), NULL)); - -sign_ctx->keytype = -#ifdef SIGN_HAVE_ED25519 - EVP_PKEY_type(EVP_PKEY_id(sign_ctx->key)) == EVP_PKEY_ED25519 - ? KEYTYPE_ED25519 : KEYTYPE_RSA; -#else - KEYTYPE_RSA; -#endif -return NULL; -} - - - -/* allocate mem for signature (when signing) */ -/* hash & sign data. Incremental not supported. - -Return: NULL for success with the signaature in the sig blob, or an error string */ - -const uschar * -exim_dkim_sign(es_ctx * sign_ctx, hashmethod hash, blob * data, blob * sig) -{ -const EVP_MD * md; -EVP_MD_CTX * ctx; -size_t siglen; - -switch (hash) - { - case HASH_NULL: md = NULL; break; /* Ed25519 signing */ - case HASH_SHA1: md = EVP_sha1(); break; - case HASH_SHA2_256: md = EVP_sha256(); break; - case HASH_SHA2_512: md = EVP_sha512(); break; - default: return US"nonhandled hash type"; - } - -#ifdef SIGN_HAVE_ED25519 -if ( (ctx = EVP_MD_CTX_new()) - && EVP_DigestSignInit(ctx, NULL, md, NULL, sign_ctx->key) > 0 - && EVP_DigestSign(ctx, NULL, &siglen, NULL, 0) > 0 - && (sig->data = store_get(siglen, GET_UNTAINTED)) - - /* Obtain the signature (slen could change here!) */ - && EVP_DigestSign(ctx, sig->data, &siglen, data->data, data->len) > 0 - ) - { - EVP_MD_CTX_destroy(ctx); - sig->len = siglen; - return NULL; - } -#else -/*XXX renamed to EVP_MD_CTX_new() in 1.1.0 */ -if ( (ctx = EVP_MD_CTX_create()) - && EVP_DigestSignInit(ctx, NULL, md, NULL, sign_ctx->key) > 0 - && EVP_DigestSignUpdate(ctx, data->data, data->len) > 0 - && EVP_DigestSignFinal(ctx, NULL, &siglen) > 0 - && (sig->data = store_get(siglen, GET_UNTAINTED)) - - /* Obtain the signature (slen could change here!) */ - && EVP_DigestSignFinal(ctx, sig->data, &siglen) > 0 - ) - { - EVP_MD_CTX_destroy(ctx); - sig->len = siglen; - return NULL; - } -#endif - -if (ctx) EVP_MD_CTX_destroy(ctx); -return US ERR_error_string(ERR_get_error(), NULL); -} - - - -/* import public key (from blob in memory) -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_verify_init(blob * pubkey, keyformat fmt, ev_ctx * verify_ctx, - unsigned * bits) -{ -const uschar * s = pubkey->data; -uschar * ret = NULL; - -switch(fmt) - { - case KEYFMT_DER: - /*XXX hmm, we never free this */ - if (!(verify_ctx->key = d2i_PUBKEY(NULL, &s, pubkey->len))) - ret = US ERR_error_string(ERR_get_error(), NULL); - break; -#ifdef SIGN_HAVE_ED25519 - case KEYFMT_ED25519_BARE: - if (!(verify_ctx->key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, - s, pubkey->len))) - ret = US ERR_error_string(ERR_get_error(), NULL); - break; -#endif - default: - ret = US"pubkey format not handled"; - break; - } - -if (!ret && bits) *bits = EVP_PKEY_bits(verify_ctx->key); -return ret; -} - - - - -/* verify signature (of hash, except Ed25519 where of-data) -(given pubkey & alleged sig) -Return: NULL for success, or an error string */ - -const uschar * -exim_dkim_verify(ev_ctx * verify_ctx, hashmethod hash, blob * data, blob * sig) -{ -const EVP_MD * md; - -switch (hash) - { - case HASH_NULL: md = NULL; break; - case HASH_SHA1: md = EVP_sha1(); break; - case HASH_SHA2_256: md = EVP_sha256(); break; - case HASH_SHA2_512: md = EVP_sha512(); break; - default: return US"nonhandled hash type"; - } - -#ifdef SIGN_HAVE_ED25519 -if (!md) - { - EVP_MD_CTX * ctx; - - if ((ctx = EVP_MD_CTX_new())) - { - if ( EVP_DigestVerifyInit(ctx, NULL, md, NULL, verify_ctx->key) > 0 - && EVP_DigestVerify(ctx, sig->data, sig->len, data->data, data->len) > 0 - ) - { EVP_MD_CTX_free(ctx); return NULL; } - EVP_MD_CTX_free(ctx); - } - } -else -#endif - { - EVP_PKEY_CTX * ctx; - - if ((ctx = EVP_PKEY_CTX_new(verify_ctx->key, NULL))) - { - if ( EVP_PKEY_verify_init(ctx) > 0 - && EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) > 0 - && EVP_PKEY_CTX_set_signature_md(ctx, md) > 0 - && EVP_PKEY_verify(ctx, sig->data, sig->len, - data->data, data->len) == 1 - ) - { EVP_PKEY_CTX_free(ctx); return NULL; } - EVP_PKEY_CTX_free(ctx); - - DEBUG(D_tls) - if (Ustrcmp(ERR_reason_error_string(ERR_peek_error()), "wrong signature length") == 0) - debug_printf("sig len (from msg hdr): %d, expected (from dns pubkey) %d\n", - (int) sig->len, EVP_PKEY_size(verify_ctx->key)); - } - } - -return US ERR_error_string(ERR_get_error(), NULL); -} - - - -#endif -/******************************************************************************/ - -#endif /*DISABLE_DKIM*/ -#endif /*MACRO_PREDEF*/ -/* End of File */ diff --git a/src/src/pdkim/signing.h b/src/src/pdkim/signing.h deleted file mode 100644 index 7760ce73f..000000000 --- a/src/src/pdkim/signing.h +++ /dev/null @@ -1,98 +0,0 @@ -/* - * PDKIM - a RFC4871 (DKIM) implementation - * - * Copyright (C) 1995 - 2020 Exim maintainers - * SPDX-License-Identifier: GPL-2.0-or-later - * - * RSA signing/verification interface - */ - -#include "../exim.h" - -#ifndef DISABLE_DKIM /* entire file */ - -#include "crypt_ver.h" - -#ifdef SIGN_OPENSSL -# include -# include -# include -#elif defined(SIGN_GNUTLS) -# include -# include -# include -#elif defined(SIGN_GCRYPT) -# include -# include -#endif - -#include "../blob.h" - -typedef enum { - KEYTYPE_RSA, - KEYTYPE_ED25519 -} keytype; - -typedef enum { - KEYFMT_DER, /* an asn.1 structure */ - KEYFMT_ED25519_BARE /* just the key */ -} keyformat; - - -#ifdef SIGN_OPENSSL - -typedef struct { - keytype keytype; - EVP_PKEY * key; -} es_ctx; - -typedef struct { - keytype keytype; - EVP_PKEY * key; -} ev_ctx; - -#elif defined(SIGN_GNUTLS) - -typedef struct { - keytype keytype; - gnutls_privkey_t key; -} es_ctx; - -typedef struct { - keytype keytype; - gnutls_pubkey_t key; -} ev_ctx; - -#elif defined(SIGN_GCRYPT) - -typedef struct { - keytype keytype; - gcry_mpi_t n; - gcry_mpi_t e; - gcry_mpi_t d; - gcry_mpi_t p; - gcry_mpi_t q; - gcry_mpi_t dp; - gcry_mpi_t dq; - gcry_mpi_t qp; -} es_ctx; - -typedef struct { - keytype keytype; - gcry_mpi_t n; - gcry_mpi_t e; -} ev_ctx; - -#endif - - -extern void exim_dkim_init(void); -extern gstring * exim_dkim_data_append(gstring *, uschar *); - -extern const uschar * exim_dkim_signing_init(const uschar *, es_ctx *); -extern const uschar * exim_dkim_sign(es_ctx *, hashmethod, blob *, blob *); -extern const uschar * exim_dkim_verify_init(blob *, keyformat, ev_ctx *, unsigned *); -extern const uschar * exim_dkim_verify(ev_ctx *, hashmethod, blob *, blob *); - -#endif /*DISABLE_DKIM*/ -/* End of File */ diff --git a/src/src/readconf.c b/src/src/readconf.c index ae7073229..5aabd0194 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -48,7 +48,7 @@ static optionlist optionlist_config[] = { { "acl_smtp_data_prdr", opt_stringptr, {&acl_smtp_data_prdr} }, #endif #ifndef DISABLE_DKIM - { "acl_smtp_dkim", opt_stringptr, {&acl_smtp_dkim} }, + { "acl_smtp_dkim", opt_module, {US"dkim"} }, #endif { "acl_smtp_etrn", opt_stringptr, {&acl_smtp_etrn} }, { "acl_smtp_expn", opt_stringptr, {&acl_smtp_expn} }, @@ -122,11 +122,11 @@ static optionlist optionlist_config[] = { #endif { "disable_ipv6", opt_bool, {&disable_ipv6} }, #ifndef DISABLE_DKIM - { "dkim_verify_hashes", opt_stringptr, {&dkim_verify_hashes} }, - { "dkim_verify_keytypes", opt_stringptr, {&dkim_verify_keytypes} }, - { "dkim_verify_min_keysizes", opt_stringptr, {&dkim_verify_min_keysizes} }, - { "dkim_verify_minimal", opt_bool, {&dkim_verify_minimal} }, - { "dkim_verify_signers", opt_stringptr, {&dkim_verify_signers} }, + { "dkim_verify_hashes", opt_module, {US"dkim"} }, + { "dkim_verify_keytypes", opt_module, {US"dkim"} }, + { "dkim_verify_min_keysizes", opt_module, {US"dkim"} }, + { "dkim_verify_minimal", opt_module, {US"dkim"} }, + { "dkim_verify_signers", opt_module, {US"dkim"} }, #endif #ifdef SUPPORT_DMARC { "dmarc_forensic_sender", opt_module, {US"dmarc"} }, diff --git a/src/src/receive.c b/src/src/receive.c index a6b7722bf..541e9320d 100644 --- a/src/src/receive.c +++ b/src/src/receive.c @@ -1828,13 +1828,6 @@ mime_is_rfc822 = 0; mime_part_count = -1; #endif -#ifndef DISABLE_DKIM -/* Call into DKIM to set up the context. In CHUNKING mode -we clear the dot-stuffing flag */ -if (smtp_input && !smtp_batched_input && !f.dkim_disable_verify) - dkim_exim_verify_init(chunking_state <= CHUNKING_OFFERED); -#endif - if (misc_mod_msg_init() != OK) goto TIDYUP; @@ -3517,100 +3510,47 @@ else #ifndef DISABLE_DKIM if (!f.dkim_disable_verify) { - /* Finish off the body hashes, calculate sigs and do compares */ - dkim_exim_verify_finish(); + misc_module_info * mi = misc_mod_findonly(US"dkim"); + if (mi) + { + typedef void (*vfin_fn_t)(void); + typedef int (*vacl_fn_t)(uschar **, uschar**); + typedef void (*vlog_fn_t)(void); - /* Check if we must run the DKIM ACL */ - GET_OPTION("acl_smtp_dkim"); - if (acl_smtp_dkim && dkim_verify_signers && *dkim_verify_signers) - { - uschar * dkim_verify_signers_expanded = - expand_string(dkim_verify_signers); - gstring * results = NULL, * seen_items = NULL; - int signer_sep = 0, old_pool = store_pool; - const uschar * ptr; - uschar * item; - - store_pool = POOL_PERM; /* Allow created variables to live to data ACL */ - - if (!(ptr = dkim_verify_signers_expanded)) - log_write(0, LOG_MAIN|LOG_PANIC, - "expansion of dkim_verify_signers option failed: %s", - expand_string_message); - - /* Loop over signers we want to verify, calling ACL. Default to OK - when no signers are present. Each call from here expands to a n ACL - call per matching sig in the message. */ - - rc = OK; - while ((item = string_nextinlist(&ptr, &signer_sep, NULL, 0))) - { - /* Prevent running ACL for an empty item */ - if (!item || !*item) continue; + /* Finish off the body hashes, calculate sigs and do compares */ - /* Only run ACL once for each domain or identity, - no matter how often it appears in the expanded list. */ - if (seen_items) - { - uschar * seen_item; - const uschar * seen_items_list = string_from_gstring(seen_items); - int seen_sep = ':'; - BOOL seen_this_item = FALSE; - - while ((seen_item = string_nextinlist(&seen_items_list, &seen_sep, - NULL, 0))) - if (Ustrcmp(seen_item,item) == 0) - { - seen_this_item = TRUE; - break; - } - - if (seen_this_item) - { - DEBUG(D_receive) - debug_printf("acl_smtp_dkim: skipping signer %s, " - "already seen\n", item); - continue; - } + (((vfin_fn_t *) mi->functions)[DKIM_VERIFY_FINISH]) (); - seen_items = string_catn(seen_items, US":", 1); - } - seen_items = string_cat(seen_items, item); + /* Check if we must run the DKIM ACL */ + + GET_OPTION("acl_smtp_dkim"); + if (acl_smtp_dkim) + { + rc = (((vacl_fn_t *) mi->functions)[DKIM_ACL_ENTRY]) + (&user_msg, &log_msg); + add_acl_headers(ACL_WHERE_DKIM, US"DKIM"); - rc = dkim_exim_acl_run(item, &results, &user_msg, &log_msg); if (rc != OK) { - DEBUG(D_receive) - debug_printf("acl_smtp_dkim: acl_check returned %d on %s, " - "skipping remaining items\n", rc, item); cancel_cutthrough_connection(TRUE, US"dkim acl not ok"); - break; + + if (rc != DISCARD) + { + Uunlink(spool_name); + if (smtp_handle_acl_fail(ACL_WHERE_DKIM, rc, user_msg, log_msg) != 0) + smtp_yield = FALSE; /* No more msgs after dropped conn */ + smtp_reply = US""; /* Indicate reply already sent */ + goto NOT_ACCEPTED; /* Skip to end of function */ + } + recipients_count = 0; + blackholed_by = US"DKIM ACL"; + if (log_msg) + blackhole_log_msg = string_sprintf(": %s", log_msg); } - else - if (dkim_verify_minimal && Ustrcmp(dkim_verify_status, "pass") == 0) - break; - } - dkim_verify_status = string_from_gstring(results); - store_pool = old_pool; - add_acl_headers(ACL_WHERE_DKIM, US"DKIM"); - if (rc == DISCARD) - { - recipients_count = 0; - blackholed_by = US"DKIM ACL"; - if (log_msg) - blackhole_log_msg = string_sprintf(": %s", log_msg); - } - else if (rc != OK) - { - Uunlink(spool_name); - if (smtp_handle_acl_fail(ACL_WHERE_DKIM, rc, user_msg, log_msg) != 0) - smtp_yield = FALSE; /* No more messages after dropped connection */ - smtp_reply = US""; /* Indicate reply already sent */ - goto NOT_ACCEPTED; /* Skip to end of function */ } - } - else /* No acl or no wanted signers */ - dkim_exim_verify_log_all(); + else /* No ACL; just log */ + (((vlog_fn_t *) mi->functions)[DKIM_VERIFY_LOG_ALL]) (); + } } #endif /* DISABLE_DKIM */ @@ -4189,8 +4129,13 @@ if (LOGGING(8bitmime)) g = string_fmt_append(g, " M8S=%d", body_8bitmime); #ifndef DISABLE_DKIM -if (LOGGING(dkim) && dkim_verify_overall) - g = string_append(g, 2, US" DKIM=", dkim_verify_overall); +if (LOGGING(dkim)) + { + misc_module_info * mi = misc_mod_findonly(US"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); diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c index adf6c59cb..e75894850 100644 --- a/src/src/smtp_in.c +++ b/src/src/smtp_in.c @@ -464,6 +464,23 @@ smtp_had_eof = smtp_had_error = 0; +#ifndef DISABLE_DKIM +/* Feed received message data to the dkim module */ +/*XXX maybe a global dkim_info? */ +void +smtp_verify_feed(const uschar * s, unsigned n) +{ +static misc_module_info * dkim_mi = NULL; +typedef void (*fn_t)(const uschar *, int); + +if (!dkim_mi && !(dkim_mi = misc_mod_findonly(US"dkim"))) + return; + +(((fn_t *) dkim_mi->functions)[DKIM_VERIFY_FEED]) (s, n); +} +#endif + + /* Refill the buffer, and notify DKIM verification code. Return false for error or EOF. */ @@ -507,7 +524,7 @@ if (rc <= 0) return FALSE; } #ifndef DISABLE_DKIM -dkim_exim_verify_feed(smtp_inbuffer, rc); +smtp_verify_feed(smtp_inbuffer, rc); #endif smtp_inend = smtp_inbuffer + rc; smtp_inptr = smtp_inbuffer; @@ -570,7 +587,7 @@ int n = smtp_inend - smtp_inptr; if (n > lim) n = lim; if (n > 0) - dkim_exim_verify_feed(smtp_inptr, n); + smtp_verify_feed(smtp_inptr, n); #endif } @@ -726,19 +743,24 @@ bdat_getc(unsigned lim) uschar * user_msg = NULL; uschar * log_msg; -for(;;) - { #ifndef DISABLE_DKIM - unsigned dkim_save; +misc_module_info * dkim_info = misc_mod_findonly(US"dkim"); +typedef void (*dkim_pause_t)(BOOL); +dkim_pause_t dkim_pause; + +dkim_pause = dkim_info + ? ((dkim_pause_t *) dkim_info->functions)[DKIM_VERIFY_PAUSE] : NULL; #endif +for(;;) + { + if (chunking_data_left > 0) return lwr_receive_getc(chunking_data_left--); bdat_pop_receive_functions(); #ifndef DISABLE_DKIM - dkim_save = dkim_collect_input; - dkim_collect_input = 0; + if (dkim_pause) dkim_pause(TRUE); #endif /* Unless PIPELINING was offered, there should be no next command @@ -767,9 +789,7 @@ for(;;) if (chunking_state == CHUNKING_LAST) { #ifndef DISABLE_DKIM - dkim_collect_input = dkim_save; - dkim_exim_verify_feed(NULL, 0); /* notify EOD */ - dkim_collect_input = 0; + smtp_verify_feed(NULL, 0); /* notify EOD */ #endif return EOD; } @@ -843,7 +863,7 @@ next_cmd: bdat_push_receive_functions(); #ifndef DISABLE_DKIM - dkim_collect_input = dkim_save; + if (dkim_pause) dkim_pause(FALSE); #endif break; /* to top of main loop */ } @@ -1681,17 +1701,6 @@ bmi_run = 0; bmi_verdicts = NULL; #endif dnslist_domain = dnslist_matched = NULL; -#ifndef DISABLE_DKIM -dkim_cur_signer = dkim_signers = -dkim_signing_domain = dkim_signing_selector = dkim_signatures = NULL; -f.dkim_disable_verify = FALSE; -dkim_collect_input = 0; -dkim_verify_overall = dkim_verify_status = dkim_verify_reason = NULL; -dkim_key_length = 0; -#endif -#ifdef SUPPORT_DMARC -f.dmarc_has_been_checked = f.dmarc_disable_verify = f.dmarc_enable_forensic = FALSE; -#endif #ifdef EXPERIMENTAL_ARC arc_state = arc_state_reason = NULL; arc_received_instance = 0; diff --git a/src/src/spool_in.c b/src/src/spool_in.c index bb54571be..43b30986d 100644 --- a/src/src/spool_in.c +++ b/src/src/spool_in.c @@ -269,9 +269,18 @@ bmi_verdicts = NULL; #endif #ifndef DISABLE_DKIM -dkim_signers = NULL; f.dkim_disable_verify = FALSE; +# ifdef COMPILE_UTILITY +dkim_signers = NULL; dkim_collect_input = 0; +#else + { + misc_module_info * mi = misc_mod_findonly(US"dkim"); + /* We used to clear only dkim_signers, dkim_collect_input. This does more + but I think it is safe. */ + if (mi) mi->smtp_reset(); + } +# endif #endif #ifndef DISABLE_TLS diff --git a/src/src/string.c b/src/src/string.c index 2b62233d8..dfda2d405 100644 --- a/src/src/string.c +++ b/src/src/string.c @@ -1688,7 +1688,7 @@ while (*fp) case '}' : zg = string_catn(zg, US"{BC}", 4); break; default: { - unsigned char u = *s; + uschar u = *s; if ( (u < 32) || (u > 127) ) zg = string_fmt_append(zg, "{%02x}", u); else diff --git a/src/src/structs.h b/src/src/structs.h index 46abac728..ef311b677 100644 --- a/src/src/structs.h +++ b/src/src/structs.h @@ -1026,6 +1026,7 @@ typedef struct misc_module_info { int (*conn_init)(const uschar *, const uschar *); void (*smtp_reset)(void); int (*msg_init)(void); + gstring * (*authres)(gstring *); void * options; unsigned options_count; diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index 06cd4a5f8..7963e2c97 100644 --- a/src/src/tls-gnu.c +++ b/src/src/tls-gnu.c @@ -3904,7 +3904,7 @@ else if (inbytes < 0) return FALSE; } #ifndef DISABLE_DKIM -dkim_exim_verify_feed(state->xfer_buffer, inbytes); +smtp_verify_feed(state->xfer_buffer, inbytes); #endif state->xfer_buffer_hwm = (int) inbytes; state->xfer_buffer_lwm = 0; @@ -3980,7 +3980,7 @@ int n = state->xfer_buffer_hwm - state->xfer_buffer_lwm; if (n > lim) n = lim; if (n > 0) - dkim_exim_verify_feed(state->xfer_buffer+state->xfer_buffer_lwm, n); + smtp_verify_feed(state->xfer_buffer+state->xfer_buffer_lwm, n); #endif } diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c index 033bd0e10..302404b6c 100644 --- a/src/src/tls-openssl.c +++ b/src/src/tls-openssl.c @@ -4545,7 +4545,7 @@ switch(error) } #ifndef DISABLE_DKIM -dkim_exim_verify_feed(ssl_xfer_buffer, inbytes); +smtp_verify_feed(ssl_xfer_buffer, inbytes); #endif ssl_xfer_buffer_hwm = inbytes; ssl_xfer_buffer_lwm = 0; @@ -4615,7 +4615,7 @@ int n = ssl_xfer_buffer_hwm - ssl_xfer_buffer_lwm; if (n > lim) n = lim; if (n > 0) - dkim_exim_verify_feed(ssl_xfer_buffer+ssl_xfer_buffer_lwm, n); + smtp_verify_feed(ssl_xfer_buffer+ssl_xfer_buffer_lwm, n); #endif } diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c index d25a2b1f6..cdd2a404f 100644 --- a/src/src/transports/smtp.c +++ b/src/src/transports/smtp.c @@ -46,6 +46,7 @@ optionlist smtp_transport_options[] = { { "data_timeout", opt_time, LOFF(data_timeout) }, { "delay_after_cutoff", opt_bool, LOFF(delay_after_cutoff) }, #ifndef DISABLE_DKIM + /*XXX dkim module */ { "dkim_canon", opt_stringptr, LOFF(dkim.dkim_canon) }, { "dkim_domain", opt_stringptr, LOFF(dkim.dkim_domain) }, { "dkim_hash", opt_stringptr, LOFF(dkim.dkim_hash) }, @@ -4103,13 +4104,18 @@ else #ifndef DISABLE_DKIM { + typedef void (*fn_t)(void); + misc_module_info * mi; # ifdef MEASURE_TIMING struct timeval t0; gettimeofday(&t0, NULL); # endif - dkim_exim_sign_init(); -# ifdef EXPERIMENTAL_ARC + + if ((mi = misc_mod_find(US"dkim", NULL))) { + (((fn_t *) mi->functions)[DKIM_TRANSPORT_INIT]) (); + +# ifdef EXPERIMENTAL_ARC uschar * s = ob->arc_sign; if (s) { @@ -4129,8 +4135,8 @@ else ob->dkim.force_bodyhash = TRUE; } } +# endif /*ARC*/ } -# endif # ifdef MEASURE_TIMING report_time_since(&t0, US"dkim_exim_sign_init (delta)"); # endif @@ -4175,7 +4181,15 @@ else } #ifndef DISABLE_DKIM - sx->ok = dkim_transport_write_message(&tctx, &ob->dkim, CUSS &message); + { + misc_module_info * mi = misc_mod_find(US"dkim", NULL); + typedef BOOL (*fn_t)(transport_ctx *, struct ob_dkim *, const uschar **); + + sx->ok = mi + ? (((fn_t *) mi->functions)[DKIM_TRANSPORT_WRITE]) + (&tctx, &ob->dkim, CUSS &message) + : transport_write_message(&tctx, 0); + } #else sx->ok = transport_write_message(&tctx, 0); #endif diff --git a/test/runtest b/test/runtest index 70499312d..83659ea19 100755 --- a/test/runtest +++ b/test/runtest @@ -1560,8 +1560,8 @@ RESET_AFTER_EXTRA_LINE_READ: # Not all platforms build with SPF enabled next if /(^$time_pid?spf_conn_init|spf_compile\.c)/; next if /try option spf_smtp_comment_template$/; - next if /loading module '(?:dmarc|spf)'$/; - next if /^$time_pid?Loaded "(?:dmarc|spf)"$/; + next if /loading module '(?:dkim|dmarc|spf)'$/; + next if /^$time_pid?Loaded "(?:dkim|dmarc|spf)"$/; # Not all platforms have sendfile support next if /^cannot use sendfile for body: no support$/; @@ -4147,7 +4147,6 @@ system("sudo cp eximdir/exim eximdir/exim_exim;" . "sudo chmod 06755 eximdir/exim_exim"); # Copy any libraries that were built for dynamic load -# Currently this is only for lookup methods ($parm_exim_dir) = $parm_exim =~ m?^(.*)/exim?;