From: Wietse Venema Date: Sun, 31 Mar 2013 05:00:00 +0000 (-0500) Subject: postfix-2.11-20130331 X-Git-Tag: v2.11.0-RC1~42 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=47fe8483a499441ea7470b57ad6f82f529650875;p=thirdparty%2Fpostfix.git postfix-2.11-20130331 --- diff --git a/postfix/.indent.pro b/postfix/.indent.pro index 41daed59e..8979235c9 100644 --- a/postfix/.indent.pro +++ b/postfix/.indent.pro @@ -278,6 +278,7 @@ -TSMTP_SASL_AUTH_CACHE -TSMTP_SESSION -TSMTP_STATE +-TSMTP_TLS_SESS -TSMTP_TLS_SITE_POLICY -TSM_STATE -TSOCKADDR_SIZE diff --git a/postfix/HISTORY b/postfix/HISTORY index 6ef1a2bdc..ec8a2b775 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -18383,3 +18383,20 @@ Apologies for any names omitted. Export tls_fprint() and tls_digest_encode() for use in DANE. Viktor Dukhovni. Files: src/tls/tls.h, src/tls/tls_fprint.c. + +20130331 + + Refactoring: TLS verification callback processing in + preparation for DANE support. Viktor Dukhovni. Files: + src/tls/tls.h, src/tls/tls_client.c, src/tls/tls_misc.c, + src/tls/tls_verify.c. + + Refactoring: split off SMTP client per-session TLS policy + data and code in preparation for DANE support. Viktor + Dukhovni. Files: src/smtp/Makefile.in, src/smtp/smtp.h, + src/smtp/smtp_connect.c, src/smtp/smtp_proto.c, + src/smtp/smtp_reuse.c, src/smtp/smtp_session.c, + src/smtp/smtp_tls_sess.c. + + Cleanup: "zero time limit" corner case in read_wait() and + write_wait() emulation. Files: util/poll_fd.c, util/iostuff.h. diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 78e976e07..85a1359a4 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20130327" +#define MAIL_RELEASE_DATE "20130331" #define MAIL_VERSION_NUMBER "2.11" #ifdef SNAPSHOT diff --git a/postfix/src/smtp/Makefile.in b/postfix/src/smtp/Makefile.in index ef017fdcc..98ba32cbf 100644 --- a/postfix/src/smtp/Makefile.in +++ b/postfix/src/smtp/Makefile.in @@ -1,10 +1,10 @@ SHELL = /bin/sh SRCS = smtp.c smtp_connect.c smtp_proto.c smtp_chat.c smtp_session.c \ - smtp_addr.c smtp_trouble.c smtp_state.c smtp_rcpt.c \ + smtp_addr.c smtp_trouble.c smtp_state.c smtp_rcpt.c smtp_tls_sess.c \ smtp_sasl_proto.c smtp_sasl_glue.c smtp_reuse.c smtp_map11.c \ smtp_sasl_auth_cache.c OBJS = smtp.o smtp_connect.o smtp_proto.o smtp_chat.o smtp_session.o \ - smtp_addr.o smtp_trouble.o smtp_state.o smtp_rcpt.o \ + smtp_addr.o smtp_trouble.o smtp_state.o smtp_rcpt.o smtp_tls_sess.o \ smtp_sasl_proto.o smtp_sasl_glue.o smtp_reuse.o smtp_map11.o \ smtp_sasl_auth_cache.o HDRS = smtp.h smtp_sasl.h smtp_addr.h smtp_reuse.h smtp_sasl_auth_cache.h @@ -591,6 +591,39 @@ smtp_state.o: ../../include/vstring.h smtp_state.o: smtp.h smtp_state.o: smtp_sasl.h smtp_state.o: smtp_state.c +smtp_tls_sess.o: ../../include/argv.h +smtp_tls_sess.o: ../../include/attr.h +smtp_tls_sess.o: ../../include/deliver_request.h +smtp_tls_sess.o: ../../include/dict.h +smtp_tls_sess.o: ../../include/dsn.h +smtp_tls_sess.o: ../../include/dsn_buf.h +smtp_tls_sess.o: ../../include/header_body_checks.h +smtp_tls_sess.o: ../../include/header_opts.h +smtp_tls_sess.o: ../../include/htable.h +smtp_tls_sess.o: ../../include/mail_params.h +smtp_tls_sess.o: ../../include/maps.h +smtp_tls_sess.o: ../../include/match_list.h +smtp_tls_sess.o: ../../include/mime_state.h +smtp_tls_sess.o: ../../include/msg.h +smtp_tls_sess.o: ../../include/msg_stats.h +smtp_tls_sess.o: ../../include/myflock.h +smtp_tls_sess.o: ../../include/mymalloc.h +smtp_tls_sess.o: ../../include/name_code.h +smtp_tls_sess.o: ../../include/name_mask.h +smtp_tls_sess.o: ../../include/recipient_list.h +smtp_tls_sess.o: ../../include/resolve_clnt.h +smtp_tls_sess.o: ../../include/scache.h +smtp_tls_sess.o: ../../include/string_list.h +smtp_tls_sess.o: ../../include/stringops.h +smtp_tls_sess.o: ../../include/sys_defs.h +smtp_tls_sess.o: ../../include/tls.h +smtp_tls_sess.o: ../../include/tok822.h +smtp_tls_sess.o: ../../include/valid_hostname.h +smtp_tls_sess.o: ../../include/vbuf.h +smtp_tls_sess.o: ../../include/vstream.h +smtp_tls_sess.o: ../../include/vstring.h +smtp_tls_sess.o: smtp.h +smtp_tls_sess.o: smtp_tls_sess.c smtp_trouble.o: ../../include/argv.h smtp_trouble.o: ../../include/attr.h smtp_trouble.o: ../../include/bounce.h diff --git a/postfix/src/smtp/smtp.h b/postfix/src/smtp/smtp.h index cef94edb9..03dbf926b 100644 --- a/postfix/src/smtp/smtp.h +++ b/postfix/src/smtp/smtp.h @@ -200,6 +200,17 @@ extern HBC_CHECKS *smtp_body_checks; /* limited body checks */ /* * smtp_session.c */ +#ifdef USE_TLS +typedef struct SMTP_TLS_SESS { + int level; /* TLS enforcement level */ + char *protocols; /* Acceptable SSL protocols */ + char *grade; /* Cipher grade: "export", ... */ + VSTRING *exclusions; /* Excluded SSL ciphers */ + ARGV *matchargv; /* Cert match patterns */ +} SMTP_TLS_SESS; + +#endif + typedef struct SMTP_SESSION { VSTREAM *stream; /* network connection */ char *dest; /* nexthop or fallback */ @@ -239,14 +250,10 @@ typedef struct SMTP_SESSION { * TLS related state, don't forget to initialize in session_tls_init()! */ #ifdef USE_TLS - TLS_SESS_STATE *tls_context; /* TLS session state */ + TLS_SESS_STATE *tls_context; /* TLS library session state */ char *tls_nexthop; /* Nexthop domain for cert checks */ - int tls_level; /* TLS enforcement level */ int tls_retry_plain; /* Try plain when TLS handshake fails */ - char *tls_protocols; /* Acceptable SSL protocols */ - char *tls_grade; /* Cipher grade: "export", ... */ - VSTRING *tls_exclusions; /* Excluded SSL ciphers */ - ARGV *tls_matchargv; /* Cert match patterns */ + SMTP_TLS_SESS *tls; /* SMTP session TLS policy */ #endif SMTP_STATE *state; /* back link */ @@ -261,6 +268,13 @@ extern SMTP_SESSION *smtp_session_activate(int, VSTRING *, VSTRING *); #ifdef USE_TLS extern void smtp_tls_list_init(void); + /* + * smtp_tls_sess.c + */ +extern SMTP_TLS_SESS *smtp_tls_sess_alloc(const char *, const char *, + unsigned, int); +extern SMTP_TLS_SESS *smtp_tls_sess_free(SMTP_TLS_SESS *); + #endif /* diff --git a/postfix/src/smtp/smtp_connect.c b/postfix/src/smtp/smtp_connect.c index c04974206..21b5e4252 100644 --- a/postfix/src/smtp/smtp_connect.c +++ b/postfix/src/smtp/smtp_connect.c @@ -520,10 +520,10 @@ static void smtp_connect_local(SMTP_STATE *state, const char *path) session->state = state; #ifdef USE_TLS session->tls_nexthop = var_myhostname; /* for TLS_LEV_SECURE */ - if (session->tls_level == TLS_LEV_MAY) { + if (session->tls->level == TLS_LEV_MAY) { msg_warn("%s: opportunistic TLS encryption is not appropriate " "for unix-domain destinations.", myname); - session->tls_level = TLS_LEV_NONE; + session->tls->level = TLS_LEV_NONE; } #endif /* All delivery errors bounce or defer. */ @@ -902,9 +902,9 @@ static void smtp_connect_inet(SMTP_STATE *state, const char *nexthop, #ifdef USE_TLS /* Disable TLS when retrying after a handshake failure */ if (retry_plain) { - if (session->tls_level >= TLS_LEV_ENCRYPT) + if (session->tls->level >= TLS_LEV_ENCRYPT) msg_panic("Plain-text retry wrong for mandatory TLS"); - session->tls_level = TLS_LEV_NONE; + session->tls->level = TLS_LEV_NONE; retry_plain = 0; } session->tls_nexthop = domain; /* for TLS_LEV_SECURE */ diff --git a/postfix/src/smtp/smtp_proto.c b/postfix/src/smtp/smtp_proto.c index aa3fe4080..929c74a67 100644 --- a/postfix/src/smtp/smtp_proto.c +++ b/postfix/src/smtp/smtp_proto.c @@ -329,7 +329,7 @@ int smtp_helo(SMTP_STATE *state) * now. */ #ifdef USE_TLS - if (session->tls_level == TLS_LEV_INVALID) + if (session->tls->level == TLS_LEV_INVALID) /* Warning is already logged. */ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "4.7.0"), @@ -638,14 +638,14 @@ int smtp_helo(SMTP_STATE *state) */ if ((session->features & SMTP_FEATURE_STARTTLS) && var_smtp_tls_note_starttls_offer && - session->tls_level <= TLS_LEV_NONE) + session->tls->level <= TLS_LEV_NONE) msg_info("Host offered STARTTLS: [%s]", session->host); /* * Decide whether or not to send STARTTLS. */ if ((session->features & SMTP_FEATURE_STARTTLS) != 0 - && smtp_tls_ctx != 0 && session->tls_level >= TLS_LEV_MAY) { + && smtp_tls_ctx != 0 && session->tls->level >= TLS_LEV_MAY) { /* * Prepare for disaster. @@ -685,7 +685,7 @@ int smtp_helo(SMTP_STATE *state) * although support for it was announced in the EHLO response. */ session->features &= ~SMTP_FEATURE_STARTTLS; - if (session->tls_level >= TLS_LEV_ENCRYPT) + if (session->tls->level >= TLS_LEV_ENCRYPT) return (smtp_site_fail(state, session->host, resp, "TLS is required, but host %s refused to start TLS: %s", session->namaddr, @@ -700,7 +700,7 @@ int smtp_helo(SMTP_STATE *state) * block. When TLS is required we must never, ever, end up in * plain-text mode. */ - if (session->tls_level >= TLS_LEV_ENCRYPT) { + if (session->tls->level >= TLS_LEV_ENCRYPT) { if (!(session->features & SMTP_FEATURE_STARTTLS)) { return (smtp_site_fail(state, DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "4.7.4"), @@ -797,17 +797,17 @@ static int smtp_start_tls(SMTP_STATE *state) ctx = smtp_tls_ctx, stream = session->stream, timeout = var_smtp_starttls_tmout, - tls_level = session->tls_level, + tls_level = session->tls->level, nexthop = session->tls_nexthop, host = session->host, namaddr = session->namaddrport, serverid = vstring_str(serverid), helo = session->helo, - protocols = session->tls_protocols, - cipher_grade = session->tls_grade, + protocols = session->tls->protocols, + cipher_grade = session->tls->grade, cipher_exclusions - = vstring_str(session->tls_exclusions), - matchargv = session->tls_matchargv, + = vstring_str(session->tls->exclusions), + matchargv = session->tls->matchargv, mdalg = var_smtp_tls_fpt_dgst); vstring_free(serverid); @@ -829,7 +829,7 @@ static int smtp_start_tls(SMTP_STATE *state) * plaintext connections, then we don't want delivery to fail with * "relay access denied". */ - if (session->tls_level == TLS_LEV_MAY + if (session->tls->level == TLS_LEV_MAY #ifdef USE_SASL_AUTH && !(var_smtp_sasl_enable && *var_smtp_sasl_passwd @@ -851,12 +851,12 @@ static int smtp_start_tls(SMTP_STATE *state) * See src/tls/tls_level.c. Levels above encrypt require matching. Levels >= * verify require CA trust. */ - if (session->tls_level >= TLS_LEV_VERIFY) + if (session->tls->level >= TLS_LEV_VERIFY) if (!TLS_CERT_IS_TRUSTED(session->tls_context)) return (smtp_site_fail(state, DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "4.7.5"), "Server certificate not trusted")); - if (session->tls_level >= TLS_LEV_DANE) + if (session->tls->level >= TLS_LEV_DANE) if (!TLS_CERT_IS_MATCHED(session->tls_context)) return (smtp_site_fail(state, DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "4.7.5"), diff --git a/postfix/src/smtp/smtp_reuse.c b/postfix/src/smtp/smtp_reuse.c index 9737025df..108b51ddc 100644 --- a/postfix/src/smtp/smtp_reuse.c +++ b/postfix/src/smtp/smtp_reuse.c @@ -198,7 +198,7 @@ static SMTP_SESSION *smtp_reuse_common(SMTP_STATE *state, int fd, * lookups, because we can compute the TLS policy much earlier. */ #ifdef USE_TLS - if (session->tls_level >= TLS_LEV_ENCRYPT) { + if (session->tls->level >= TLS_LEV_ENCRYPT) { if (msg_verbose) msg_info("%s: skipping plain-text cached session to %s", myname, label); diff --git a/postfix/src/smtp/smtp_session.c b/postfix/src/smtp/smtp_session.c index 5b72402f5..8390cccac 100644 --- a/postfix/src/smtp/smtp_session.c +++ b/postfix/src/smtp/smtp_session.c @@ -132,436 +132,6 @@ #include "smtp.h" #include "smtp_sasl.h" -#ifdef USE_TLS - -static MAPS *tls_policy; /* lookup table(s) */ -static MAPS *tls_per_site; /* lookup table(s) */ - -/* smtp_tls_list_init - initialize per-site policy lists */ - -void smtp_tls_list_init(void) -{ - if (*var_smtp_tls_policy) { - tls_policy = maps_create(SMTP_X(TLS_POLICY), var_smtp_tls_policy, - DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); - if (*var_smtp_tls_per_site) - msg_warn("%s ignored when %s is not empty.", - SMTP_X(TLS_PER_SITE), SMTP_X(TLS_POLICY)); - return; - } - if (*var_smtp_tls_per_site) { - tls_per_site = maps_create(SMTP_X(TLS_PER_SITE), var_smtp_tls_per_site, - DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); - } -} - -/* policy_name - printable tls policy level */ - -static const char *policy_name(int tls_level) -{ - const char *name = str_tls_level(tls_level); - - if (name == 0) - name = "unknown"; - return name; -} - -/* tls_site_lookup - look up per-site TLS security level */ - -static void tls_site_lookup(int *site_level, const char *site_name, - const char *site_class) -{ - const char *lookup; - - /* - * Look up a non-default policy. In case of multiple lookup results, the - * precedence order is a permutation of the TLS enforcement level order: - * VERIFY, ENCRYPT, NONE, MAY, NOTFOUND. I.e. we override MAY with a more - * specific policy including NONE, otherwise we choose the stronger - * enforcement level. - */ - if ((lookup = maps_find(tls_per_site, site_name, 0)) != 0) { - if (!strcasecmp(lookup, "NONE")) { - /* NONE overrides MAY or NOTFOUND. */ - if (*site_level <= TLS_LEV_MAY) - *site_level = TLS_LEV_NONE; - } else if (!strcasecmp(lookup, "MAY")) { - /* MAY overrides NOTFOUND but not NONE. */ - if (*site_level < TLS_LEV_NONE) - *site_level = TLS_LEV_MAY; - } else if (!strcasecmp(lookup, "MUST_NOPEERMATCH")) { - if (*site_level < TLS_LEV_ENCRYPT) - *site_level = TLS_LEV_ENCRYPT; - } else if (!strcasecmp(lookup, "MUST")) { - if (*site_level < TLS_LEV_VERIFY) - *site_level = TLS_LEV_VERIFY; - } else { - msg_warn("Table %s: ignoring unknown TLS policy '%s' for %s %s", - var_smtp_tls_per_site, lookup, site_class, site_name); - } - } else if (tls_per_site->error) { - msg_fatal("%s lookup error for %s", tls_per_site->title, site_name); - } -} - -/* tls_policy_lookup_one - look up destination TLS policy */ - -static int tls_policy_lookup_one(SMTP_SESSION *session, int *site_level, - const char *site_name, - const char *site_class) -{ - const char *lookup; - char *policy; - char *saved_policy; - char *tok; - const char *err; - char *name; - char *val; - static VSTRING *cbuf; - -#undef FREE_RETURN -#define FREE_RETURN(x) do { myfree(saved_policy); return (x); } while (0) - - if ((lookup = maps_find(tls_policy, site_name, 0)) == 0) { - if (tls_policy->error) { - msg_fatal("%s: %s lookup error for %s", - session->state->request->queue_id, - tls_policy->title, site_name); - /* XXX session->stream has no longjmp context yet. */ - } - return (0); - } - if (cbuf == 0) - cbuf = vstring_alloc(10); - -#define WHERE \ - vstring_str(vstring_sprintf(cbuf, "TLS policy table, %s \"%s\"", \ - site_class, site_name)) - - saved_policy = policy = mystrdup(lookup); - - if ((tok = mystrtok(&policy, "\t\n\r ,")) == 0) { - msg_warn("%s: invalid empty policy", WHERE); - *site_level = TLS_LEV_INVALID; - FREE_RETURN(1); /* No further lookups */ - } - *site_level = tls_level_lookup(tok); - if (*site_level == TLS_LEV_INVALID) { - /* tls_level_lookup() logs no warning. */ - msg_warn("%s: invalid security level \"%s\"", WHERE, tok); - FREE_RETURN(1); /* No further lookups */ - } - - /* - * Warn about ignored attributes when TLS is disabled. - */ - if (*site_level < TLS_LEV_MAY) { - while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) - msg_warn("%s: ignoring attribute \"%s\" with TLS disabled", - WHERE, tok); - FREE_RETURN(1); - } - - /* - * Errors in attributes may have security consequences, don't ignore - * errors that can degrade security. - */ - while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) { - if ((err = split_nameval(tok, &name, &val)) != 0) { - *site_level = TLS_LEV_INVALID; - msg_warn("%s: malformed attribute/value pair \"%s\": %s", - WHERE, tok, err); - break; - } - /* Only one instance per policy. */ - if (!strcasecmp(name, "ciphers")) { - if (*val == 0) { - msg_warn("%s: attribute \"%s\" has empty value", WHERE, name); - *site_level = TLS_LEV_INVALID; - break; - } - if (session->tls_grade) { - msg_warn("%s: attribute \"%s\" is specified multiple times", - WHERE, name); - *site_level = TLS_LEV_INVALID; - break; - } - session->tls_grade = mystrdup(val); - continue; - } - /* Only one instance per policy. */ - if (!strcasecmp(name, "protocols")) { - if (session->tls_protocols) { - msg_warn("%s: attribute \"%s\" is specified multiple times", - WHERE, name); - *site_level = TLS_LEV_INVALID; - break; - } - session->tls_protocols = mystrdup(val); - continue; - } - /* Multiple instance(s) per policy. */ - if (!strcasecmp(name, "match")) { - char *delim = *site_level == TLS_LEV_FPRINT ? "|" : ":"; - - if (*site_level <= TLS_LEV_ENCRYPT) { - msg_warn("%s: attribute \"%s\" invalid at security level \"%s\"", - WHERE, name, policy_name(*site_level)); - *site_level = TLS_LEV_INVALID; - break; - } - if (*val == 0) { - msg_warn("%s: attribute \"%s\" has empty value", WHERE, name); - *site_level = TLS_LEV_INVALID; - break; - } - if (session->tls_matchargv == 0) - session->tls_matchargv = argv_split(val, delim); - else - argv_split_append(session->tls_matchargv, val, delim); - continue; - } - /* Only one instance per policy. */ - if (!strcasecmp(name, "exclude")) { - if (session->tls_exclusions) { - msg_warn("%s: attribute \"%s\" is specified multiple times", - WHERE, name); - *site_level = TLS_LEV_INVALID; - break; - } - session->tls_exclusions = vstring_strcpy(vstring_alloc(10), val); - continue; - } else { - msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name); - *site_level = TLS_LEV_INVALID; - break; - } - } - FREE_RETURN(1); -} - -/* tls_policy_lookup - look up destination TLS policy */ - -static void tls_policy_lookup(SMTP_SESSION *session, int *site_level, - const char *site_name, - const char *site_class) -{ - - /* - * Only one lookup with [nexthop]:port, [nexthop] or nexthop:port These - * are never the domain part of localpart@domain, rather they are - * explicit nexthops from transport:nexthop, and match only the - * corresponding policy. Parent domain matching (below) applies only to - * sub-domains of the recipient domain. - */ - if (!valid_hostname(site_name, DONT_GRIPE)) { - tls_policy_lookup_one(session, site_level, site_name, site_class); - return; - } - - /* - * XXX For clarity consider using ``do { .. } while'', instead of using - * ``while { .. }'' with loop control at the bottom. - */ - while (1) { - /* Try the given domain */ - if (tls_policy_lookup_one(session, site_level, site_name, site_class)) - return; - /* Re-try with parent domain */ - if ((site_name = strchr(site_name + 1, '.')) == 0) - return; - } -} - -/* set_cipher_grade - Set cipher grade and exclusions */ - -static void set_cipher_grade(SMTP_SESSION *session) -{ - const char *mand_exclude = ""; - const char *also_exclude = ""; - - /* - * Use main.cf cipher level if no per-destination value specified. With - * mandatory encryption at least encrypt, and with mandatory verification - * at least authenticate! - */ - switch (session->tls_level) { - case TLS_LEV_INVALID: - case TLS_LEV_NONE: - return; - - case TLS_LEV_MAY: - if (session->tls_grade == 0) - session->tls_grade = mystrdup(var_smtp_tls_ciph); - break; - - case TLS_LEV_ENCRYPT: - if (session->tls_grade == 0) - session->tls_grade = mystrdup(var_smtp_tls_mand_ciph); - mand_exclude = var_smtp_tls_mand_excl; - also_exclude = "eNULL"; - break; - - case TLS_LEV_FPRINT: - case TLS_LEV_VERIFY: - case TLS_LEV_SECURE: - if (session->tls_grade == 0) - session->tls_grade = mystrdup(var_smtp_tls_mand_ciph); - mand_exclude = var_smtp_tls_mand_excl; - also_exclude = "aNULL"; - break; - } - -#define ADD_EXCLUDE(vstr, str) \ - do { \ - if (*(str)) \ - vstring_sprintf_append((vstr), "%s%s", \ - VSTRING_LEN(vstr) ? " " : "", (str)); \ - } while (0) - - /* - * The "exclude" policy table attribute overrides main.cf exclusion - * lists. - */ - if (session->tls_exclusions == 0) { - session->tls_exclusions = vstring_alloc(10); - ADD_EXCLUDE(session->tls_exclusions, var_smtp_tls_excl_ciph); - ADD_EXCLUDE(session->tls_exclusions, mand_exclude); - } - ADD_EXCLUDE(session->tls_exclusions, also_exclude); -} - -/* session_tls_init - session TLS parameters */ - -static void session_tls_init(SMTP_SESSION *session, const char *dest, - const char *host, int flags) -{ - const char *myname = "session_tls_init"; - int global_level; - int site_level; - - /* - * Initialize all TLS related session properties. - */ - session->tls_context = 0; - session->tls_nexthop = 0; - session->tls_level = TLS_LEV_NONE; - session->tls_retry_plain = 0; - session->tls_protocols = 0; - session->tls_grade = 0; - session->tls_exclusions = 0; - session->tls_matchargv = 0; - - /* - * Compute the global TLS policy. This is the default policy level when - * no per-site policy exists. It also is used to override a wild-card - * per-site policy. - */ - if (*var_smtp_tls_level) { - /* Require that var_smtp_tls_level is sanitized upon startup. */ - global_level = tls_level_lookup(var_smtp_tls_level); - if (global_level == TLS_LEV_INVALID) - msg_panic("%s: invalid TLS security level: \"%s\"", - myname, var_smtp_tls_level); - } else if (var_smtp_enforce_tls) { - global_level = var_smtp_tls_enforce_peername ? - TLS_LEV_VERIFY : TLS_LEV_ENCRYPT; - } else { - global_level = var_smtp_use_tls ? - TLS_LEV_MAY : TLS_LEV_NONE; - } - if (msg_verbose) - msg_info("%s TLS level: %s", "global", policy_name(global_level)); - - /* - * Compute the per-site TLS enforcement level. For compatibility with the - * original TLS patch, this algorithm is gives equal precedence to host - * and next-hop policies. - */ - site_level = TLS_LEV_NOTFOUND; - - if (tls_policy) { - tls_policy_lookup(session, &site_level, dest, "next-hop destination"); - } else if (tls_per_site) { - tls_site_lookup(&site_level, dest, "next-hop destination"); - if (strcasecmp(dest, host) != 0) - tls_site_lookup(&site_level, host, "server hostname"); - if (msg_verbose) - msg_info("%s TLS level: %s", "site", policy_name(site_level)); - - /* - * Override a wild-card per-site policy with a more specific global - * policy. - * - * With the original TLS patch, 1) a per-site ENCRYPT could not override - * a global VERIFY, and 2) a combined per-site (NONE+MAY) policy - * produced inconsistent results: it changed a global VERIFY into - * NONE, while producing MAY with all weaker global policy settings. - * - * With the current implementation, a combined per-site (NONE+MAY) - * consistently overrides global policy with NONE, and global policy - * can override only a per-site MAY wildcard. That is, specific - * policies consistently override wildcard policies, and - * (non-wildcard) per-site policies consistently override global - * policies. - */ - if (site_level == TLS_LEV_MAY && global_level > TLS_LEV_MAY) - site_level = global_level; - } - if (site_level == TLS_LEV_NOTFOUND) - session->tls_level = global_level; - else - session->tls_level = site_level; - - /* - * Use main.cf protocols setting if not set in per-destination table. - */ - if (session->tls_level > TLS_LEV_NONE && session->tls_protocols == 0) - session->tls_protocols = - mystrdup((session->tls_level == TLS_LEV_MAY) ? - var_smtp_tls_proto : var_smtp_tls_mand_proto); - - /* - * Compute cipher grade (if set in per-destination table, else - * set_cipher() uses main.cf settings) and security level dependent - * cipher exclusion list. - */ - set_cipher_grade(session); - - /* - * Use main.cf cert_match setting if not set in per-destination table. - */ - if (session->tls_matchargv == 0) { - switch (session->tls_level) { - case TLS_LEV_INVALID: - case TLS_LEV_NONE: - case TLS_LEV_MAY: - case TLS_LEV_ENCRYPT: - break; - case TLS_LEV_FPRINT: - session->tls_matchargv = - argv_split(var_smtp_tls_fpt_cmatch, "\t\n\r, |"); - break; - case TLS_LEV_VERIFY: - session->tls_matchargv = - argv_split(var_smtp_tls_vfy_cmatch, "\t\n\r, :"); - break; - case TLS_LEV_SECURE: - session->tls_matchargv = - argv_split(var_smtp_tls_sec_cmatch, "\t\n\r, :"); - break; - default: - msg_panic("unexpected TLS security level: %d", - session->tls_level); - } - } - if (msg_verbose && (tls_policy || tls_per_site)) - msg_info("%s TLS level: %s", "effective", - policy_name(session->tls_level)); -} - -#endif - /* smtp_session_alloc - allocate and initialize SMTP_SESSION structure */ SMTP_SESSION *smtp_session_alloc(VSTREAM *stream, const char *dest, @@ -609,13 +179,12 @@ SMTP_SESSION *smtp_session_alloc(VSTREAM *stream, const char *dest, smtp_sasl_connect(session); #endif - /* - * Need to pass the session as a parameter when the new-style per-nexthop - * policies can specify not only security level thresholds, but also how - * security levels are defined. - */ #ifdef USE_TLS - session_tls_init(session, dest, host, flags); + session->tls_context = 0; + session->tls_retry_plain = 0; + session->tls_nexthop = 0; + session->tls = + smtp_tls_sess_alloc(dest, host, port, flags & SMTP_MISC_FLAG_TLSA_HOST); #endif session->state = 0; debug_peer_check(host, addr); @@ -633,14 +202,8 @@ void smtp_session_free(SMTP_SESSION *session) tls_client_stop(smtp_tls_ctx, session->stream, var_smtp_starttls_tmout, 0, session->tls_context); } - if (session->tls_protocols) - myfree(session->tls_protocols); - if (session->tls_grade) - myfree(session->tls_grade); - if (session->tls_exclusions) - vstring_free(session->tls_exclusions); - if (session->tls_matchargv) - argv_free(session->tls_matchargv); + if (session->tls) + (void) smtp_tls_sess_free(session->tls); #endif if (session->stream) vstream_fclose(session->stream); diff --git a/postfix/src/smtp/smtp_tls_sess.c b/postfix/src/smtp/smtp_tls_sess.c new file mode 100644 index 000000000..f127a803a --- /dev/null +++ b/postfix/src/smtp/smtp_tls_sess.c @@ -0,0 +1,537 @@ +/*++ +/* NAME +/* smtp_tls_sess 3 +/* SUMMARY +/* SMTP_TLS_SESS structure management +/* SYNOPSIS +/* #include "smtp.h" +/* +/* void smtp_tls_list_init() +/* +/* SMTP_TLS_SESS *smtp_tls_sess_alloc(dest, host, port, valid) +/* char *dest; +/* char *host; +/* unsigned port; +/* int valid; +/* +/* SMTP_TLS_SESS *smtp_tls_sess_free(tls) +/* SMTP_TLS_SESS *tls; +/* DESCRIPTION +/* smtp_tls_list_init() initializes lookup tables used by the TLS +/* policy engine. +/* +/* smtp_tls_sess_alloc() allocates memory for an SMTP_TLS_SESS structure +/* and initializes it based on the given information. NOTE: the +/* port is in network byte order. +/* +/* smtp_tls_sess_free() destroys an SMTP_TLS_SESS structure and its +/* members. A null pointer is returned for convenience. +/* +/* Arguments: +/* .IP dest +/* The unmodified next-hop or fall-back destination including +/* the optional [] and including the optional port or service. +/* .IP host +/* The name of the host that we are connected to. If the name was +/* obtained via an MX lookup and/or CNAME expansion (any indirection), +/* all those lookups must also have been validated. If the hostname +/* is validated, it must be the final name after all CNAME expansion. +/* Otherwise, it is generally the original name or that returned by +/* the MX lookup (see smtp_cname_overrides_servername). +/* .IP port +/* The remote port, network byte order. +/* .IP valid +/* The DNSSEC validation status of the host name. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Viktor Dukhovni +/* +/* TLS support originally by: +/* Lutz Jaenicke +/* BTU Cottbus +/* Allgemeine Elektrotechnik +/* Universitaetsplatz 3-4 +/* D-03044 Cottbus, Germany +/*--*/ + +/* System library. */ + +#include + +#ifdef USE_TLS + +#include +#include + +#ifdef STRCASECMP_IN_STRINGS_H +#include +#endif + +/* Utility library. */ + +#include +#include +#include +#include +#include + +/* Global library. */ + +#include +#include + +/* Application-specific. */ + +#include "smtp.h" + +static MAPS *tls_policy; /* lookup table(s) */ +static MAPS *tls_per_site; /* lookup table(s) */ + +/* smtp_tls_list_init - initialize per-site policy lists */ + +void smtp_tls_list_init(void) +{ + if (*var_smtp_tls_policy) { + tls_policy = maps_create(SMTP_X(TLS_POLICY), var_smtp_tls_policy, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); + if (*var_smtp_tls_per_site) + msg_warn("%s ignored when %s is not empty.", + SMTP_X(TLS_PER_SITE), SMTP_X(TLS_POLICY)); + return; + } + if (*var_smtp_tls_per_site) { + tls_per_site = maps_create(SMTP_X(TLS_PER_SITE), var_smtp_tls_per_site, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); + } +} + +/* policy_name - printable tls policy level */ + +static const char *policy_name(int tls_level) +{ + const char *name = str_tls_level(tls_level); + + if (name == 0) + name = "unknown"; + return name; +} + +/* tls_site_lookup - look up per-site TLS security level */ + +static void tls_site_lookup(int *site_level, const char *site_name, + const char *site_class) +{ + const char *lookup; + + /* + * Look up a non-default policy. In case of multiple lookup results, the + * precedence order is a permutation of the TLS enforcement level order: + * VERIFY, ENCRYPT, NONE, MAY, NOTFOUND. I.e. we override MAY with a more + * specific policy including NONE, otherwise we choose the stronger + * enforcement level. + */ + if ((lookup = maps_find(tls_per_site, site_name, 0)) != 0) { + if (!strcasecmp(lookup, "NONE")) { + /* NONE overrides MAY or NOTFOUND. */ + if (*site_level <= TLS_LEV_MAY) + *site_level = TLS_LEV_NONE; + } else if (!strcasecmp(lookup, "MAY")) { + /* MAY overrides NOTFOUND but not NONE. */ + if (*site_level < TLS_LEV_NONE) + *site_level = TLS_LEV_MAY; + } else if (!strcasecmp(lookup, "MUST_NOPEERMATCH")) { + if (*site_level < TLS_LEV_ENCRYPT) + *site_level = TLS_LEV_ENCRYPT; + } else if (!strcasecmp(lookup, "MUST")) { + if (*site_level < TLS_LEV_VERIFY) + *site_level = TLS_LEV_VERIFY; + } else { + msg_warn("Table %s: ignoring unknown TLS policy '%s' for %s %s", + var_smtp_tls_per_site, lookup, site_class, site_name); + } + } else if (tls_per_site->error) { + msg_fatal("%s lookup error for %s", tls_per_site->title, site_name); + } +} + +/* tls_policy_lookup_one - look up destination TLS policy */ + +static int tls_policy_lookup_one(SMTP_TLS_SESS *tls, int *site_level, + const char *site_name, + const char *site_class) +{ + const char *lookup; + char *policy; + char *saved_policy; + char *tok; + const char *err; + char *name; + char *val; + static VSTRING *cbuf; + +#undef FREE_RETURN +#define FREE_RETURN(x) do { myfree(saved_policy); return (x); } while (0) + + if ((lookup = maps_find(tls_policy, site_name, 0)) == 0) { + if (tls_policy->error) { + msg_fatal("%s lookup error for %s", + tls_policy->title, site_name); + /* XXX session->stream has no longjmp context yet. */ + } + return (0); + } + if (cbuf == 0) + cbuf = vstring_alloc(10); + +#define WHERE \ + vstring_str(vstring_sprintf(cbuf, "TLS policy table, %s \"%s\"", \ + site_class, site_name)) + + saved_policy = policy = mystrdup(lookup); + + if ((tok = mystrtok(&policy, "\t\n\r ,")) == 0) { + msg_warn("%s: invalid empty policy", WHERE); + *site_level = TLS_LEV_INVALID; + FREE_RETURN(1); /* No further lookups */ + } + *site_level = tls_level_lookup(tok); + if (*site_level == TLS_LEV_INVALID) { + /* tls_level_lookup() logs no warning. */ + msg_warn("%s: invalid security level \"%s\"", WHERE, tok); + FREE_RETURN(1); /* No further lookups */ + } + + /* + * Warn about ignored attributes when TLS is disabled. + */ + if (*site_level < TLS_LEV_MAY) { + while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) + msg_warn("%s: ignoring attribute \"%s\" with TLS disabled", + WHERE, tok); + FREE_RETURN(1); + } + + /* + * Errors in attributes may have security consequences, don't ignore + * errors that can degrade security. + */ + while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) { + if ((err = split_nameval(tok, &name, &val)) != 0) { + *site_level = TLS_LEV_INVALID; + msg_warn("%s: malformed attribute/value pair \"%s\": %s", + WHERE, tok, err); + break; + } + /* Only one instance per policy. */ + if (!strcasecmp(name, "ciphers")) { + if (*val == 0) { + msg_warn("%s: attribute \"%s\" has empty value", WHERE, name); + *site_level = TLS_LEV_INVALID; + break; + } + if (tls->grade) { + msg_warn("%s: attribute \"%s\" is specified multiple times", + WHERE, name); + *site_level = TLS_LEV_INVALID; + break; + } + tls->grade = mystrdup(val); + continue; + } + /* Only one instance per policy. */ + if (!strcasecmp(name, "protocols")) { + if (tls->protocols) { + msg_warn("%s: attribute \"%s\" is specified multiple times", + WHERE, name); + *site_level = TLS_LEV_INVALID; + break; + } + tls->protocols = mystrdup(val); + continue; + } + /* Multiple instances per policy. */ + if (!strcasecmp(name, "match")) { + char *delim = *site_level == TLS_LEV_FPRINT ? "|" : ":"; + + if (*site_level <= TLS_LEV_ENCRYPT) { + msg_warn("%s: attribute \"%s\" invalid at security level " + "\"%s\"", WHERE, name, policy_name(*site_level)); + *site_level = TLS_LEV_INVALID; + break; + } + if (*val == 0) { + msg_warn("%s: attribute \"%s\" has empty value", WHERE, name); + *site_level = TLS_LEV_INVALID; + break; + } + if (tls->matchargv == 0) + tls->matchargv = argv_split(val, delim); + else + argv_split_append(tls->matchargv, val, delim); + continue; + } + /* Only one instance per policy. */ + if (!strcasecmp(name, "exclude")) { + if (tls->exclusions) { + msg_warn("%s: attribute \"%s\" is specified multiple times", + WHERE, name); + *site_level = TLS_LEV_INVALID; + break; + } + tls->exclusions = vstring_strcpy(vstring_alloc(10), val); + continue; + } else { + msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name); + *site_level = TLS_LEV_INVALID; + break; + } + } + FREE_RETURN(1); +} + +/* tls_policy_lookup - look up destination TLS policy */ + +static void tls_policy_lookup(SMTP_TLS_SESS *tls, int *site_level, + const char *site_name, + const char *site_class) +{ + + /* + * Only one lookup with [nexthop]:port, [nexthop] or nexthop:port These + * are never the domain part of localpart@domain, rather they are + * explicit nexthops from transport:nexthop, and match only the + * corresponding policy. Parent domain matching (below) applies only to + * sub-domains of the recipient domain. + */ + if (!valid_hostname(site_name, DONT_GRIPE)) { + tls_policy_lookup_one(tls, site_level, site_name, site_class); + return; + } + + /* + * XXX For clarity consider using ``do { .. } while'', instead of using + * ``while { .. }'' with loop control at the bottom. + */ + while (1) { + /* Try the given domain */ + if (tls_policy_lookup_one(tls, site_level, site_name, site_class)) + return; + /* Re-try with parent domain */ + if ((site_name = strchr(site_name + 1, '.')) == 0) + return; + } +} + +/* set_cipher_grade - Set cipher grade and exclusions */ + +static void set_cipher_grade(SMTP_TLS_SESS *tls) +{ + const char *mand_exclude = ""; + const char *also_exclude = ""; + + /* + * Use main.cf cipher level if no per-destination value specified. With + * mandatory encryption at least encrypt, and with mandatory verification + * at least authenticate! + */ + switch (tls->level) { + case TLS_LEV_INVALID: + case TLS_LEV_NONE: + return; + + case TLS_LEV_MAY: + if (tls->grade == 0) + tls->grade = mystrdup(var_smtp_tls_ciph); + break; + + case TLS_LEV_ENCRYPT: + if (tls->grade == 0) + tls->grade = mystrdup(var_smtp_tls_mand_ciph); + mand_exclude = var_smtp_tls_mand_excl; + also_exclude = "eNULL"; + break; + + case TLS_LEV_FPRINT: + case TLS_LEV_VERIFY: + case TLS_LEV_SECURE: + if (tls->grade == 0) + tls->grade = mystrdup(var_smtp_tls_mand_ciph); + mand_exclude = var_smtp_tls_mand_excl; + also_exclude = "aNULL"; + break; + } + +#define ADD_EXCLUDE(vstr, str) \ + do { \ + if (*(str)) \ + vstring_sprintf_append((vstr), "%s%s", \ + VSTRING_LEN(vstr) ? " " : "", (str)); \ + } while (0) + + /* + * The "exclude" policy table attribute overrides main.cf exclusion + * lists. + */ + if (tls->exclusions == 0) { + tls->exclusions = vstring_alloc(10); + ADD_EXCLUDE(tls->exclusions, var_smtp_tls_excl_ciph); + ADD_EXCLUDE(tls->exclusions, mand_exclude); + } + ADD_EXCLUDE(tls->exclusions, also_exclude); +} + +/* smtp_tls_sess_alloc - session TLS policy parameters */ + +SMTP_TLS_SESS *smtp_tls_sess_alloc(const char *dest, const char *host, + unsigned port, int valid) +{ + const char *myname = "session_tls_init"; + int global_level; + int site_level; + SMTP_TLS_SESS *tls = (SMTP_TLS_SESS *) mymalloc(sizeof(*tls)); + + tls->level = TLS_LEV_NONE; + tls->protocols = 0; + tls->grade = 0; + tls->exclusions = 0; + tls->matchargv = 0; + + /* + * Compute the global TLS policy. This is the default policy level when + * no per-site policy exists. It also is used to override a wild-card + * per-site policy. + */ + if (*var_smtp_tls_level) { + /* Require that var_smtp_tls_level is sanitized upon startup. */ + global_level = tls_level_lookup(var_smtp_tls_level); + if (global_level == TLS_LEV_INVALID) + msg_panic("%s: invalid TLS security level: \"%s\"", + myname, var_smtp_tls_level); + } else if (var_smtp_enforce_tls) { + global_level = var_smtp_tls_enforce_peername ? + TLS_LEV_VERIFY : TLS_LEV_ENCRYPT; + } else { + global_level = var_smtp_use_tls ? + TLS_LEV_MAY : TLS_LEV_NONE; + } + if (msg_verbose) + msg_info("%s TLS level: %s", "global", policy_name(global_level)); + + /* + * Compute the per-site TLS enforcement level. For compatibility with the + * original TLS patch, this algorithm is gives equal precedence to host + * and next-hop policies. + */ + site_level = TLS_LEV_NOTFOUND; + + if (tls_policy) { + tls_policy_lookup(tls, &site_level, dest, "next-hop destination"); + } else if (tls_per_site) { + tls_site_lookup(&site_level, dest, "next-hop destination"); + if (strcasecmp(dest, host) != 0) + tls_site_lookup(&site_level, host, "server hostname"); + if (msg_verbose) + msg_info("%s TLS level: %s", "site", policy_name(site_level)); + + /* + * Override a wild-card per-site policy with a more specific global + * policy. + * + * With the original TLS patch, 1) a per-site ENCRYPT could not override + * a global VERIFY, and 2) a combined per-site (NONE+MAY) policy + * produced inconsistent results: it changed a global VERIFY into + * NONE, while producing MAY with all weaker global policy settings. + * + * With the current implementation, a combined per-site (NONE+MAY) + * consistently overrides global policy with NONE, and global policy + * can override only a per-site MAY wildcard. That is, specific + * policies consistently override wildcard policies, and + * (non-wildcard) per-site policies consistently override global + * policies. + */ + if (site_level == TLS_LEV_MAY && global_level > TLS_LEV_MAY) + site_level = global_level; + } + if (site_level == TLS_LEV_NOTFOUND) + tls->level = global_level; + else + tls->level = site_level; + + /* + * Use main.cf protocols setting if not set in per-destination table. + */ + if (tls->level > TLS_LEV_NONE && tls->protocols == 0) + tls->protocols = + mystrdup((tls->level == TLS_LEV_MAY) ? + var_smtp_tls_proto : var_smtp_tls_mand_proto); + + /* + * Compute cipher grade (if set in per-destination table, else + * set_cipher() uses main.cf settings) and security level dependent + * cipher exclusion list. + */ + set_cipher_grade(tls); + + /* + * Use main.cf cert_match setting if not set in per-destination table. + */ + switch (tls->level) { + case TLS_LEV_INVALID: + case TLS_LEV_NONE: + case TLS_LEV_MAY: + case TLS_LEV_ENCRYPT: + case TLS_LEV_DANE: + break; + case TLS_LEV_FPRINT: + if (tls->matchargv == 0) + tls->matchargv = + argv_split(var_smtp_tls_fpt_cmatch, "\t\n\r, |"); + break; + case TLS_LEV_VERIFY: + if (tls->matchargv == 0) + tls->matchargv = + argv_split(var_smtp_tls_vfy_cmatch, "\t\n\r, :"); + break; + case TLS_LEV_SECURE: + if (tls->matchargv == 0) + tls->matchargv = + argv_split(var_smtp_tls_sec_cmatch, "\t\n\r, :"); + break; + default: + msg_panic("unexpected TLS security level: %d", + tls->level); + } + + if (msg_verbose && (tls_policy || tls_per_site)) + msg_info("%s TLS level: %s", "effective", policy_name(tls->level)); + + return (tls); +} + +/* smtp_sess_tls_free - free and return null pointer of same type */ + +SMTP_TLS_SESS *smtp_tls_sess_free(SMTP_TLS_SESS *tls) +{ + + if (tls->protocols) + myfree(tls->protocols); + if (tls->grade) + myfree(tls->grade); + if (tls->exclusions) + vstring_free(tls->exclusions); + if (tls->matchargv) + argv_free(tls->matchargv); + + myfree((char *) tls); + return (0); +} + +#endif diff --git a/postfix/src/tls/tls.h b/postfix/src/tls/tls.h index 0c65743dd..a89f456ac 100644 --- a/postfix/src/tls/tls.h +++ b/postfix/src/tls/tls.h @@ -101,6 +101,9 @@ typedef struct { const char *mdalg; /* default message digest algorithm */ /* Built-in vs external SSL_accept/read/write/shutdown support. */ VSTREAM *stream; /* Blocking-mode SMTP session */ + int errordepth; /* Chain depth of error cert */ + int errorcode; /* First error at error depth */ + X509 *errorcert; /* Error certificate closest to leaf */ } TLS_SESS_STATE; /* @@ -400,6 +403,7 @@ extern char *tls_peer_CN(X509 *, const TLS_SESS_STATE *); extern char *tls_issuer_CN(X509 *, const TLS_SESS_STATE *); extern const char *tls_dns_name(const GENERAL_NAME *, const TLS_SESS_STATE *); extern int tls_verify_certificate_callback(int, X509_STORE_CTX *); +extern void tls_log_verify_error(TLS_SESS_STATE *); /* * tls_fprint.c diff --git a/postfix/src/tls/tls_client.c b/postfix/src/tls/tls_client.c index 19582ee6e..2282c4489 100644 --- a/postfix/src/tls/tls_client.c +++ b/postfix/src/tls/tls_client.c @@ -700,16 +700,19 @@ static void verify_extract_name(TLS_SESS_STATE *TLScontext, X509 *peercert, TLScontext->peer_CN = tls_peer_CN(peercert, TLScontext); /* - * Give them a clue. Problems with trust chain verification were logged - * when the session was first negotiated, before the session was stored + * Give them a clue. Problems with trust chain verification are logged + * when the session is first negotiated, before the session is stored * into the cache. We don't want mystery failures, so log the fact the * real problem is to be found in the past. */ - if (TLScontext->session_reused - && !TLS_CERT_IS_TRUSTED(TLScontext) - && (TLScontext->log_mask & TLS_LOG_UNTRUSTED)) - msg_info("%s: re-using session with untrusted certificate, " - "look for details earlier in the log", props->namaddr); + if (!TLS_CERT_IS_TRUSTED(TLScontext) + && (TLScontext->log_mask & TLS_LOG_UNTRUSTED)) { + if (TLScontext->session_reused == 0) + tls_log_verify_error(TLScontext); + else + msg_info("%s: re-using session with untrusted certificate, " + "look for details earlier in the log", props->namaddr); + } } /* verify_extract_print - extract and verify peer fingerprint */ @@ -731,7 +734,8 @@ static void verify_extract_print(TLS_SESS_STATE *TLScontext, X509 *peercert, for (cpp = props->matchargv->argv; *cpp; ++cpp) { if (strcasecmp(TLScontext->peer_fingerprint, *cpp) == 0 || strcasecmp(TLScontext->peer_pkey_fprint, *cpp) == 0) { - TLScontext->peer_status |= TLS_CERT_FLAG_MATCHED; + TLScontext->peer_status |= + TLS_CERT_FLAG_TRUSTED | TLS_CERT_FLAG_MATCHED; break; } } diff --git a/postfix/src/tls/tls_fprint.c b/postfix/src/tls/tls_fprint.c index 88f7c03fa..182b71b6e 100644 --- a/postfix/src/tls/tls_fprint.c +++ b/postfix/src/tls/tls_fprint.c @@ -215,9 +215,7 @@ char *tls_fprint(const char *buf, int len, const char *mdalg) const EVP_MD *md; unsigned char md_buf[EVP_MAX_MD_SIZE]; unsigned int md_len; - int i; int ok = 1; - char *result = 0; /* Previously available in "init" routine. */ if ((md = EVP_get_digestbyname(mdalg)) == 0) diff --git a/postfix/src/tls/tls_misc.c b/postfix/src/tls/tls_misc.c index c09e7f189..8143da296 100644 --- a/postfix/src/tls/tls_misc.c +++ b/postfix/src/tls/tls_misc.c @@ -747,6 +747,9 @@ TLS_SESS_STATE *tls_alloc_sess_context(int log_mask, const char *namaddr) TLScontext->log_mask = log_mask; TLScontext->namaddr = lowercase(mystrdup(namaddr)); TLScontext->mdalg = 0; /* Alias for props->mdalg */ + TLScontext->errordepth = -1; + TLScontext->errorcode = X509_V_OK; + TLScontext->errorcert = 0; return (TLScontext); } @@ -777,6 +780,8 @@ void tls_free_context(TLS_SESS_STATE *TLScontext) myfree(TLScontext->peer_fingerprint); if (TLScontext->peer_pkey_fprint) myfree(TLScontext->peer_pkey_fprint); + if (TLScontext->errorcert) + X509_free(TLScontext->errorcert); myfree((char *) TLScontext); } diff --git a/postfix/src/tls/tls_verify.c b/postfix/src/tls/tls_verify.c index 50dddc8cf..f3daf2a5c 100644 --- a/postfix/src/tls/tls_verify.c +++ b/postfix/src/tls/tls_verify.c @@ -7,6 +7,13 @@ /* #define TLS_INTERNAL /* #include /* +/* int tls_verify_certificate_callback(ok, ctx) +/* int ok; +/* X509_STORE_CTX *ctx; +/* +/* int tls_log_verify_error(TLScontext) +/* TLS_SESS_STATE *TLScontext; +/* /* char *tls_peer_CN(peercert, TLScontext) /* X509 *peercert; /* TLS_SESS_STATE *TLScontext; @@ -18,12 +25,19 @@ /* const char *tls_dns_name(gn, TLScontext) /* const GENERAL_NAME *gn; /* TLS_SESS_STATE *TLScontext; +/* DESCRIPTION +/* tls_verify_certificate_callback() is called several times (directly +/* or indirectly) from crypto/x509/x509_vfy.c. It collects errors +/* and trust information at each element of the trust chain. +/* The last call at depth 0 sets the verification status based +/* on the cumulative winner (lowest depth) of errors vs. trust. +/* We always return 1 (continue the handshake) and handle trust +/* and peer-name verification problems at the application level. /* +/* tls_log_verify_error() (called only when we care about the +/* peer certificate, that is not when opportunistic) logs the +/* reason why the certificate failed to be verified. /* -/* int tls_verify_certificate_callback(ok, ctx) -/* int ok; -/* X509_STORE_CTX *ctx; -/* DESCRIPTION /* tls_peer_CN() returns the text CommonName for the peer /* certificate subject, or an empty string if no CommonName was /* found. The result is allocated with mymalloc() and must be @@ -41,32 +55,6 @@ /* are found, a null string is returned instead. Further sanity /* checks may be added if the need arises. /* -/* tls_verify_callback() is called several times (directly or -/* indirectly) from crypto/x509/x509_vfy.c. It is called as -/* a final check, and if it returns "0", the handshake is -/* immediately shut down and the connection fails. -/* -/* Postfix/TLS has two modes, the "opportunistic" mode and -/* the "enforce" mode: -/* -/* In the "opportunistic" mode we never want the connection -/* to fail just because there is something wrong with the -/* peer's certificate. After all, we would have sent or received -/* the mail even if TLS weren't available. Therefore the -/* return value is always "1". -/* -/* The SMTP client or server may require TLS (e.g. to protect -/* passwords), while peer certificates are optional. In this -/* case we must return "1" even when we are unhappy with the -/* peer certificate. Only when peer certificates are required, -/* certificate verification failure will result in immediate -/* termination (return 0). -/* -/* The only error condition not handled inside the OpenSSL -/* library is the case of a too-long certificate chain. We -/* test for this condition only if "ok = 1", that is, if -/* verification didn't fail because of some earlier problem. -/* /* Arguments: /* .IP ok /* Result of prior verification: non-zero means success. In @@ -132,6 +120,29 @@ #define TLS_INTERNAL #include +/* update_error_state - safely stash away error state */ + +static void update_error_state(TLS_SESS_STATE *TLScontext, int depth, + X509 *errorcert, int errorcode) +{ + /* No news is good news */ + if (TLScontext->errordepth >= 0 && TLScontext->errordepth <= depth) + return; + + /* + * The certificate pointer is stable during the verification callback, + * but may be freed after the callback returns. Since we delay error + * reporting till later, we bump the refcount so we can rely on it still + * being there until later. + */ + if (TLScontext->errorcert != 0) + X509_free(TLScontext->errorcert); + if (errorcert != 0) + CRYPTO_add(&errorcert->references, 1, CRYPTO_LOCK_X509); + TLScontext->errorcert = errorcert; + TLScontext->errorcode = errorcode; +} + /* tls_verify_certificate_callback - verify peer certificate info */ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) @@ -140,31 +151,19 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) X509 *cert; int err; int depth; + int max_depth; SSL *con; TLS_SESS_STATE *TLScontext; - depth = X509_STORE_CTX_get_error_depth(ctx); + /* May be NULL as of OpenSSL 1.0, thanks for the API change! */ cert = X509_STORE_CTX_get_current_cert(ctx); + err = X509_STORE_CTX_get_error(ctx); con = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); TLScontext = SSL_get_ex_data(con, TLScontext_index); /* - * The callback function is called repeatedly, first with the root - * certificate, and then with each intermediate certificate ending with - * the peer certificate. - * - * With each call, the validity of the current certificate (usage bits, - * attributes, expiration, ... checked by the OpenSSL library) is - * available in the "ok" argument. Error details are available via - * X509_STORE_CTX API. - * - * We never terminate the SSL handshake in the verification callback, rather - * we allow the TLS handshake to continue, but mark the session as - * unverified. The application is responsible for closing any sessions - * with unverified credentials. - * - * Certificate chain depth limit violations are mis-reported by the OpenSSL - * library, from SSL_CTX_set_verify(3): + * Certificate chain depth limit violations are mis-reported by the + * OpenSSL library, from SSL_CTX_set_verify(3): * * The certificate verification depth set with SSL[_CTX]_verify_depth() * stops the verification at a certain depth. The error message produced @@ -176,33 +175,74 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) * present at this depth. This disambiguates trust chain truncation from * an incomplete trust chain. */ - if (depth >= SSL_get_verify_depth(con)) { + depth = X509_STORE_CTX_get_error_depth(ctx); + max_depth = SSL_get_verify_depth(con) - 1; + + /* + * We never terminate the SSL handshake in the verification callback, + * rather we allow the TLS handshake to continue, but mark the session as + * unverified. The application is responsible for closing any sessions + * with unverified credentials. + * + * When we have an explicit list of trusted CA fingerprints, record the + * smallest depth at which we find a trusted certificate. If this below + * the smallest error depth we win and the chain is trusted. Otherwise, + * the chain is untrusted. We make this decision *each* time we are + * called with depth == 0 (yes we may be called more than once). + */ + if (max_depth >= 0 && depth > max_depth) { + update_error_state(TLScontext, depth, cert, + X509_V_ERR_CERT_CHAIN_TOO_LONG); ok = 0; - X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_CHAIN_TOO_LONG); + } + if (ok == 0) + update_error_state(TLScontext, depth, cert, err); + + /* + * The final depth zero call sets the verification status. + */ + if (depth == 0) { + ok = TLScontext->errordepth < 0 ? 1 : 0; + X509_STORE_CTX_set_error(ctx, ok ? X509_V_OK : TLScontext->errorcode); } if (TLScontext->log_mask & TLS_LOG_VERBOSE) { - X509_NAME_oneline(X509_get_subject_name(cert), buf, sizeof(buf)); - msg_info("%s: certificate verification depth=%d verify=%d subject=%s", + if (cert) + X509_NAME_oneline(X509_get_subject_name(cert), buf, sizeof(buf)); + else + strcpy(buf, ""); + msg_info("%s: depth=%d verify=%d subject=%s", TLScontext->namaddr, depth, ok, printable(buf, '?')); } + return (1); +} - /* - * If no errors, or we are not logging verification errors, we are done. - */ - if (ok || (TLScontext->log_mask & TLS_LOG_UNTRUSTED) == 0) - return (1); +/* tls_log_verify_error - Report final verification error status */ - /* - * One counter-example is enough. - */ - TLScontext->log_mask &= ~TLS_LOG_UNTRUSTED; +void tls_log_verify_error(TLS_SESS_STATE *TLScontext) +{ + char buf[CCERT_BUFSIZ]; + int err = TLScontext->errorcode; + X509 *cert = TLScontext->errorcert; + int depth = TLScontext->errordepth; #define PURPOSE ((depth>0) ? "CA": TLScontext->am_server ? "client": "server") + if (err == X509_V_OK) + return; + /* * Specific causes for verification failure. */ - switch (err = X509_STORE_CTX_get_error(ctx)) { + switch (err) { + case X509_V_ERR_CERT_UNTRUSTED: + + /* + * We expect the error cert to be the leaf, but it is likely + * sufficient to omit it from the log, even less user confusion. + */ + msg_info("certificate verification failed for %s: " + "not trusted by local or TLSA policy", TLScontext->namaddr); + break; case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: msg_info("certificate verification failed for %s: " "self-signed certificate", TLScontext->namaddr); @@ -215,8 +255,10 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) * provided, but not found in CAfile/CApath. Either way, we don't * trust it. */ - X509_NAME_oneline(X509_get_issuer_name(ctx->current_cert), - buf, sizeof(buf)); + if (cert) + X509_NAME_oneline(X509_get_issuer_name(cert), buf, sizeof(buf)); + else + strcpy(buf, ""); msg_info("certificate verification failed for %s: untrusted issuer %s", TLScontext->namaddr, printable(buf, '?')); break; @@ -237,7 +279,7 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) case X509_V_ERR_CERT_CHAIN_TOO_LONG: msg_info("certificate verification failed for %s: " "certificate chain longer than limit(%d)", - TLScontext->namaddr, SSL_get_verify_depth(con) - 1); + TLScontext->namaddr, depth - 1); break; default: msg_info("%s certificate verification failed for %s: num=%d:%s", @@ -245,8 +287,6 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) X509_verify_cert_error_string(err)); break; } - - return (1); } #ifndef DONT_GRIPE diff --git a/postfix/src/util/iostuff.h b/postfix/src/util/iostuff.h index da3fa3a38..539165d7f 100644 --- a/postfix/src/util/iostuff.h +++ b/postfix/src/util/iostuff.h @@ -16,7 +16,7 @@ extern int non_blocking(int, int); extern int close_on_exec(int, int); extern int open_limit(int); -extern int poll_fd(int, int, int, int); +extern int poll_fd(int, int, int, int, int); extern off_t get_file_limit(void); extern void set_file_limit(off_t); extern ssize_t peekfd(int); @@ -33,11 +33,11 @@ extern int unix_send_fd(int, int); extern ssize_t dummy_read(int, void *, size_t, int, void *); extern ssize_t dummy_write(int, void *, size_t, int, void *); -#define readable(fd) poll_fd((fd), POLL_FD_READ, 0, 1) -#define writable(fd) poll_fd((fd), POLL_FD_WRITE, 0, 1) +#define readable(fd) poll_fd((fd), POLL_FD_READ, 0, 1, 0) +#define writable(fd) poll_fd((fd), POLL_FD_WRITE, 0, 1, 0) -#define read_wait(fd, time_limit) poll_fd((fd), POLL_FD_READ, (time_limit), 0) -#define write_wait(fd, time_limit) poll_fd((fd), POLL_FD_WRITE, (time_limit), 0) +#define read_wait(fd, timeout) poll_fd((fd), POLL_FD_READ, (timeout), 0, -1) +#define write_wait(fd, timeout) poll_fd((fd), POLL_FD_WRITE, (timeout), 0, -1) extern int inet_windowsize; extern void set_inet_windowsize(int, int); diff --git a/postfix/src/util/poll_fd.c b/postfix/src/util/poll_fd.c index 5b22b1289..e8c7ed21f 100644 --- a/postfix/src/util/poll_fd.c +++ b/postfix/src/util/poll_fd.c @@ -20,11 +20,12 @@ /* int fd; /* int time_limit; /* -/* int poll_fd(fd, request, time_limit, success_val) +/* int poll_fd(fd, request, time_limit, true_res, false_res) /* int fd; /* int request; /* int time_limit; -/* int success_val; +/* int true_res; +/* int false_res; /* DESCRIPTION /* The functions in this module are macros that provide a /* convenient interface to poll_fd(). @@ -56,25 +57,28 @@ /* value effects a poll (return immediately). A negative value /* means wait until the requested POLL_FD_READ or POLL_FD_WRITE /* condition becomes true. -/* .IP success_val +/* .IP true_res /* Result value when the requested POLL_FD_READ or POLL_FD_WRITE /* condition is true. +/* .IP false_res +/* Result value when the requested POLL_FD_READ or POLL_FD_WRITE +/* condition is false. /* DIAGNOSTICS /* Panic: interface violation. All system call errors are fatal /* unless specified otherwise. /* /* readable() and writable() return 1 when the requested -/* condition is true, zero when it is false. They never return -/* an error indication. +/* POLL_FD_READ or POLL_FD_WRITE condition is true, zero when +/* it is false. They never return an error indication. /* -/* read_wait() and write_wait() return zero when successful, -/* -1 with errno set to ETIMEDOUT when the time limit was -/* reached. +/* read_wait() and write_wait() return zero when the requested +/* POLL_FD_READ or POLL_FD_WRITE condition is true, -1 with +/* errno set to ETIMEDOUT when it is false. /* -/* poll_fd() returns -1 with errno set to ETIMEDOUT when the -/* time limit was reached, success_val if the requested -/* POLL_FD_READ or POLL_FD_WRITE condition is true, and returns -/* zero otherwise. +/* poll_fd() returns true_res when the requested POLL_FD_READ +/* or POLL_FD_WRITE condition is true, false_res when it is +/* false. When poll_fd() returns a false_res value < 0, it +/* also sets errno to ETIMEDOUT. /* LICENSE /* .ad /* .fi @@ -102,7 +106,7 @@ #define poll_fd_sysv poll_fd #define USE_SYSV_POLL #define USE_BSD_SELECT -int poll_fd_bsd(int, int, int, int); +int poll_fd_bsd(int, int, int, int, int); /* * Use select() only. @@ -141,7 +145,8 @@ int poll_fd_bsd(int, int, int, int); /* poll_fd_bsd - block with time_limit until file descriptor is ready */ -int poll_fd_bsd(int fd, int request, int time_limit, int success_val) +int poll_fd_bsd(int fd, int request, int time_limit, + int true_res, int false_res) { fd_set req_fds; fd_set *read_fds; @@ -196,16 +201,13 @@ int poll_fd_bsd(int fd, int request, int time_limit, int success_val) case 0: if (temp_fd != -1) (void) close(temp_fd); - if (time_limit == 0) { - return (0); - } else { + if (false_res < 0) errno = ETIMEDOUT; - return (-1); - } + return (false_res); default: if (temp_fd != -1) (void) close(temp_fd); - return (success_val); + return (true_res); } } } @@ -215,16 +217,17 @@ int poll_fd_bsd(int fd, int request, int time_limit, int success_val) #ifdef USE_SYSV_POLL #ifdef USE_SYSV_POLL_THEN_SELECT -#define HANDLE_SYSV_POLL_ERROR(fd, request, time_limit, success_val) \ - return (poll_fd_bsd((fd), (request), (time_limit), (success_val))) +#define HANDLE_SYSV_POLL_ERROR(fd, req, time_limit, true_res, false_res) \ + return (poll_fd_bsd((fd), (req), (time_limit), (true_res), (false_res))) #else -#define HANDLE_SYSV_POLL_ERROR(fd, request, time_limit, success_val) \ +#define HANDLE_SYSV_POLL_ERROR(fd, req, time_limit, true_res, false_res) \ msg_fatal("poll: %m") #endif /* poll_fd_sysv - block with time_limit until file descriptor is ready */ -int poll_fd_sysv(int fd, int request, int time_limit, int success_val) +int poll_fd_sysv(int fd, int request, int time_limit, + int true_res, int false_res) { struct pollfd pollfd; @@ -247,19 +250,18 @@ int poll_fd_sysv(int fd, int request, int time_limit, int success_val) WAIT_FOR_EVENT : time_limit * 1000)) { case -1: if (errno != EINTR) - HANDLE_SYSV_POLL_ERROR(fd, request, time_limit, success_val); + HANDLE_SYSV_POLL_ERROR(fd, request, time_limit, + true_res, false_res); continue; case 0: - if (time_limit == 0) { - return (0); - } else { + if (false_res < 0) errno = ETIMEDOUT; - return (-1); - } + return (false_res); default: if (pollfd.revents & POLLNVAL) - HANDLE_SYSV_POLL_ERROR(fd, request, time_limit, success_val); - return (success_val); + HANDLE_SYSV_POLL_ERROR(fd, request, time_limit, + true_res, false_res); + return (true_res); } } }