/*++
/* NAME
/* smtp_tls_policy_test 1t
/* SUMMARY
/* smtp_tls_policy unit tests
/* SYNOPSIS
/* ./smtp_tls_policy_test
/* DESCRIPTION
/* smtp_tls_policy_test runs and logs each configured test, reports
/* if a test is a PASS or FAIL, and returns an exit status of zero
/* if all tests are a PASS.
/* LICENSE
/* .ad
/* .fi
/* The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/* Wietse Venema
/* porcupine.org
/*--*/
/*
* System library .
*/
#include <sys_defs.h>
#include <stdlib.h>
/*
* Utility library .
*/
#include <argv.h>
#include <msg.h>
#include <msg_vstream.h>
#include <stringops.h>
#include <vstream.h>
#include <vstring.h>
#ifdef USE_TLS
#define PASS (0 )
#define FAIL (1 )
/*
* Global library .
*/
#include <mail_params.h>
/*
* TLS library .
*/
/*
* SMTP client .
*/
#include <smtp.h>
/*
* Surrogate dependencies . Parameters are initialized without $ name
* expansion , and can be changed by tests .
*/
char *var_smtp_tls_policy;
char *var_smtp_tls_per_site;
char *var_smtp_tls_ciph;
bool var_smtp_tls_conn_reuse;
bool var_smtp_tls_enable_rpk;
char *var_smtp_tls_level;
int smtp_host_lookup_mask;
int var_smtp_cache_conn;
char *var_smtp_tls_mand_ciph;
char *var_smtp_tls_mand_excl;
char *var_smtp_dns_res_opt;
char *var_smtp_dns_support;
char *var_smtp_tls_insecure_mx_policy;
bool var_ign_mx_lookup_err;
bool var_smtp_enforce_tls;
bool var_smtp_tls_enforce_peername;
bool var_smtp_tls_force_tlsa;
char *var_smtp_tls_fpt_cmatch;
char *var_smtp_tls_mand_proto;
char *var_smtp_tls_proto;
char *var_smtp_tls_sec_cmatch;
char *var_smtp_tls_sni;
char *var_smtp_tls_tafile;
char *var_smtp_tls_vfy_cmatch;
bool var_smtp_use_tls;
char *var_smtp_tls_excl_ciph;
bool var_smtp_tls_enf_sts_mx_pat;
bool var_smtp_tls_wrappermode;
bool var_log_tls_feature_status;
/*
* Other globals .
*/
int smtp_mode;
int smtp_tls_insecure_mx_policy;
unsigned smtp_dns_res_opt;
int smtp_dns_support;
int smtp_host_lookup_mask;
/*
* Pre - test initializer to make tests independent .
*/
static void test_setup(void )
{
var_smtp_tls_policy = DEF_SMTP_TLS_POLICY;
var_smtp_tls_per_site = DEF_SMTP_TLS_PER_SITE;
var_smtp_tls_ciph = DEF_SMTP_TLS_CIPH;
var_smtp_tls_conn_reuse = DEF_SMTP_TLS_CONN_REUSE;
var_smtp_tls_enable_rpk = DEF_SMTP_TLS_ENABLE_RPK;
var_smtp_tls_level = "may" ;
var_smtp_cache_conn = 2 ;
var_smtp_tls_mand_ciph = DEF_SMTP_TLS_MAND_CIPH;
var_smtp_tls_mand_excl = DEF_SMTP_TLS_MAND_EXCL;
var_smtp_dns_res_opt = DEF_SMTP_DNS_RES_OPT;
var_smtp_dns_support = DEF_SMTP_DNS_SUPPORT;
var_smtp_tls_insecure_mx_policy = DEF_SMTP_TLS_INSECURE_MX_POLICY;
var_ign_mx_lookup_err = DEF_IGN_MX_LOOKUP_ERR;
var_smtp_enforce_tls = DEF_SMTP_ENFORCE_TLS;
var_smtp_tls_enforce_peername = DEF_SMTP_TLS_ENFORCE_PN;
var_smtp_tls_force_tlsa = DEF_SMTP_TLS_FORCE_TLSA;
var_smtp_tls_fpt_cmatch = DEF_SMTP_TLS_FPT_CMATCH;
var_smtp_tls_mand_proto = DEF_SMTP_TLS_MAND_PROTO;
var_smtp_tls_proto = DEF_SMTP_TLS_PROTO;
var_smtp_tls_sec_cmatch = DEF_SMTP_TLS_SEC_CMATCH;
var_smtp_tls_sni = DEF_SMTP_TLS_SNI;
var_smtp_tls_tafile = DEF_SMTP_TLS_TAFILE;
var_smtp_tls_vfy_cmatch = DEF_SMTP_TLS_VFY_CMATCH;
var_smtp_use_tls = DEF_SMTP_USE_TLS;
var_smtp_tls_excl_ciph = DEF_SMTP_TLS_EXCL_CIPH;
var_smtp_tls_enf_sts_mx_pat = 1 ;
var_smtp_tls_wrappermode = 0 ;
var_tls_required_enable = 0 ;
var_log_tls_feature_status = 1 ;
smtp_mode = 1 ;
smtp_tls_policy_cache_flush();
}
/*
* Post - test finalizer to help memory leak tests .
*/
static void test_teardown(void )
{
smtp_tls_policy_cache_flush();
}
/*
* Test helpers . TODO ( wietse ) adopt PTEST which does a nicer job .
*/
static int match_int(const char *what, int want, int got)
{
if (want != got) {
msg_warn("%s: got %d, want %d" , what, got, want);
return (0 );
}
return (1 );
}
#define STR_OR_NULL(s) ((s)? (s) : "NULL" )
static int match_cstr(const char *what, const char *want, const char *got)
{
if (!want != !got) {
msg_warn("%s: got '%s', want '%s'" , what, STR_OR_NULL(got),
STR_OR_NULL(want));
return (0 );
}
if (want != 0 && strcmp(got, want) != 0 ) {
msg_warn("%s: got '%s', want '%s'" , what, got, want);
return (0 );
}
return (1 );
}
#define WANT_ARGV_MAX 5
struct WANT_ARGV {
ssize_t argc;
char *argv[WANT_ARGV_MAX];
};
#define ARGV_OR_NULL(a) ((a)? ("argv" ) : "NULL" )
static int match_argv(const char *what, const struct WANT_ARGV * want,
const ARGV *got)
{
if (!want != !got) {
msg_warn("%s: got '%s', want '%s'" , what, ARGV_OR_NULL(got),
ARGV_OR_NULL(want));
return (0 );
}
if (match_int("argc" , want->argc, got->argc) == 0 ) {
return (0 );
} else {
VSTRING *buf;
int idx;
int equal;
buf = vstring_alloc(100 );
for (equal = 1 , idx = 0 ; idx < want->argc; idx++) {
vstring_sprintf(buf, "%s->argv[%d]" , what, idx);
if (match_cstr(STR(buf), want->argv[idx], got->argv[idx]) == 0 )
equal = 0 ;
}
vstring_free(buf);
return (equal);
}
}
/*
* Limited policy for STS tests .
*/
struct WANT_SMTP_TLS_POLICY {
int level; /* TLS enforcement level */
#if 0
char *protocols; /* Acceptable SSL protocols */
char *grade; /* Cipher grade: "export", ... */
VSTRING *exclusions; /* Excluded SSL ciphers */
char *protocols; /* Acceptable SSL protocols */
char *grade; /* Cipher grade: "export", ... */
VSTRING *exclusions; /* Excluded SSL ciphers */
#endif
struct WANT_ARGV matchargv; /* Cert match patterns */
#if 0
DSN_BUF *why; /* Lookup error status */
TLS_DANE *dane; /* DANE TLSA digests */
#endif
char *sni; /* Optional SNI name when not DANE */
#if 0
int conn_reuse; /* enable connection reuse */
int enable_rpk; /* Enable server->client RPK */
#endif
int ext_policy_ttl; /* TTL from DNS etc. */
char *ext_policy_type; /* (sts) */
struct WANT_ARGV ext_policy_strings;/* policy strings from DNS etc. */
char *ext_policy_domain; /* policy scope */
struct WANT_ARGV ext_mx_host_patterns; /* (sts) MX host patterns */
char *ext_policy_failure; /* (sts) policy failure */
};
#define POLICY_OR_NULL(p) ((p)? ("policy" ) : "NULL" )
static int match_smtp_tls_policy(const char *what,
const struct WANT_SMTP_TLS_POLICY * want,
const SMTP_TLS_POLICY *got)
{
int equal = 1 ;
if (!want != !got) {
msg_warn("%s: got '%s', want '%s'" , what, POLICY_OR_NULL(got),
POLICY_OR_NULL(want));
return (0 );
}
if (match_int("level" , want->level, got->level) == 0 ) {
msg_warn("%s->level mismatch" , what);
equal = 0 ;
}
if (match_argv("matchargv" , &want->matchargv, got->matchargv) == 0 ) {
msg_warn("%s->matchargv mismatch" , what);
equal = 0 ;
}
if (match_cstr("sni" , want->sni, got->sni) == 0 ) {
msg_warn("%s->sni mismatch" , what);
equal = 0 ;
}
if (match_int("ext_policy_ttl" , want->ext_policy_ttl, got->ext_policy_ttl) == 0 ) {
msg_warn("%s->ext_policy_ttl mismatch" , what);
equal = 0 ;
}
if (match_cstr("ext_policy_type" , want->ext_policy_type,
got->ext_policy_type) == 0 ) {
msg_warn("%s->ext_policy_type mismatch" , what);
equal = 0 ;
}
if (match_argv("ext_policy_strings" , &want->ext_policy_strings,
got->ext_policy_strings) == 0 ) {
msg_warn("%s->ext_policy_strings mismatch" , what);
equal = 0 ;
}
if (match_cstr("ext_policy_domain" , want->ext_policy_domain,
got->ext_policy_domain) == 0 ) {
msg_warn("%s->ext_policy_domain mismatch" , what);
equal = 0 ;
}
if (match_argv("ext_mx_host_patterns" , &want->ext_mx_host_patterns,
got->ext_mx_host_patterns) == 0 ) {
msg_warn("%s->ext_mx_host_patterns mismatch" , what);
equal = 0 ;
}
if (match_cstr("ext_policy_failure" , want->ext_policy_failure,
got->ext_policy_failure) == 0 ) {
msg_warn("%s->ext_policy_failure mismatch" , what);
equal = 0 ;
}
return (equal);
}
/*
* Test structure . Some tests may bring their own .
*/
typedef struct TEST_CASE {
const char *label;
int (*action) (const struct TEST_CASE *);
} TEST_CASE;
/*
* Verify that policies from an STS plugin are converted into Postfix TLS
* policies that match a certificate against the server hostname , and that
* other policy attributes are stored appropriately . A later test will
* verify that MX hostnames are matched against the STS policy MX hostname
* patterns .
*/
static int sts_policy_smoke_test(const struct TEST_CASE *tp)
{
var_smtp_tls_policy =
"static:{secure match=example:.example "
"servername=example policy_ttl=123 "
"policy_type=sts policy_string=one "
"policy_string=two policy_domain=example "
"mx_host_pattern=example mx_host_pattern=*.example}" ;
static const struct WANT_SMTP_TLS_POLICY want_policy = {
.level = TLS_LEV_SECURE,
.matchargv = {.argc = 1 ,.argv = {"hostname" }},
.sni = "example" ,
.ext_policy_ttl = 123 ,
.ext_policy_type = "sts" ,
.ext_policy_strings = {.argc = 2 ,.argv = {"one" , "two" }},
.ext_policy_domain = "example" ,
.ext_mx_host_patterns = {.argc = 2 ,.argv = {"example" , "*.example" }}
};
SMTP_STATE *state = smtp_state_alloc();
const char *domain = "example" ;
const char *host = "mail.example" ;
const char *addr = "10.0.1.1" ;
int port = 25 ;
int match;
var_smtp_tls_enf_sts_mx_pat = 1 ;
var_smtp_tls_level = "secure" ;
smtp_tls_list_init();
SMTP_ITER_INIT(state->iterator, domain, host, addr, port, state);
if ((match = smtp_tls_policy_cache_query(state->why, state->tls,
state->iterator)) == 0 ) {
msg_warn("smtp_tls_policy_cache_query failed: %s" ,
STR(state->why->reason));
} else {
match = match_smtp_tls_policy("policy" , &want_policy, state->tls);
}
smtp_state_free(state);
smtp_tls_policy_cache_flush();
return (match ? PASS : FAIL);
}
/*
* Verify that the historical support for policies from STS plugins is still
* available . STS plugins generate a policy that will match a certificate
* against all MX patterns , after converting a pattern " * . domain " to
* " . domain " , and do not constrain the allowed MX hostnames .
*/
static int obs_sts_policy_smoke_test(const struct TEST_CASE *tp)
{
var_smtp_tls_policy =
"static:{secure match=example:.example "
"servername=example policy_ttl=123 "
"policy_type=sts policy_string=one "
"policy_string=two policy_domain=example "
"mx_host_pattern=example mx_host_pattern=*.example}" ;
static const struct WANT_SMTP_TLS_POLICY want_policy = {
.level = TLS_LEV_SECURE,
.matchargv = {.argc = 2 ,.argv = {"example" , ".example" }},
.sni = "example" ,
.ext_policy_ttl = 123 ,
.ext_policy_type = "sts" ,
.ext_policy_strings = {.argc = 2 ,.argv = {"one" , "two" }},
.ext_policy_domain = "example" ,
.ext_mx_host_patterns = {.argc = 2 ,.argv = {"example" , "*.example" }}
};
SMTP_STATE *state = smtp_state_alloc();
const char *domain = "example" ;
const char *host = "mail.example" ;
const char *addr = "10.0.1.1" ;
int port = 25 ;
int match;
var_smtp_tls_enf_sts_mx_pat = 0 ;
var_smtp_tls_level = "secure" ;
smtp_tls_list_init();
SMTP_ITER_INIT(state->iterator, domain, host, addr, port, state);
if ((match = smtp_tls_policy_cache_query(state->why, state->tls,
state->iterator)) == 0 ) {
msg_warn("smtp_tls_policy_cache_query failed: %s" ,
STR(state->why->reason));
} else {
match = match_smtp_tls_policy("policy" , &want_policy, state->tls);
}
smtp_state_free(state);
smtp_tls_policy_cache_flush();
return (match ? PASS : FAIL);
}
/*
* Test the MX host authorization constraints .
*/
static int test_hostname_authorization(const struct TEST_CASE *tp)
{
var_smtp_tls_policy =
"static:{secure match=example.com:.example.com "
"servername=example.com policy_ttl=123 "
"policy_type=sts policy_string=one "
"policy_string=two policy_domain=example.com "
"mx_host_pattern=example.com mx_host_pattern=*.example.com}" ;
static const struct WANT_SMTP_TLS_POLICY want_policy = {
.level = TLS_LEV_SECURE,
.matchargv = {.argc = 1 ,.argv = {"hostname" }},
.sni = "example.com" ,
.ext_policy_ttl = 123 ,
.ext_policy_type = "sts" ,
.ext_policy_strings = {.argc = 2 ,.argv = {"one" , "two" }},
.ext_policy_domain = "example.com" ,
.ext_mx_host_patterns = {.argc = 2 ,.argv = {"example.com" , "*.example.com" }}
};
SMTP_STATE *state = smtp_state_alloc();
const char *domain = "example.com" ;
const char *host = "mail.example.com" ;
const char *addr = "10.0.1.1" ;
int port = 25 ;
int match;
static const char *permit_domains[] = {
"example.com" , "mail.example.com" , 0 ,
};
static const char *reject_domains[] = {
".example.com" , "foo.bar.example.com" , 0 ,
};
const char *const * cpp;
var_smtp_tls_enf_sts_mx_pat = 1 ;
var_smtp_tls_level = "secure" ;
smtp_tls_list_init();
SMTP_ITER_INIT(state->iterator, domain, host, addr, port, state);
if ((match = smtp_tls_policy_cache_query(state->why, state->tls,
state->iterator)) == 0 ) {
msg_warn("smtp_tls_policy_cache_query failed: %s" ,
STR(state->why->reason));
} else {
match = match_smtp_tls_policy("policy" , &want_policy, state->tls);
}
if (match == 0 )
return (FAIL);
/* Verify that 'good' MX host names are authorized. */
for (cpp = permit_domains; *cpp; cpp++) {
if (!smtp_tls_authorize_mx_hostname(state->tls, *cpp)) {
msg_warn("hostname '%s' is not authorized" , *cpp);
match = 0 ;
}
}
/* Verify that 'wrong' MX host names are not authorized. */
for (cpp = reject_domains; *cpp; cpp++) {
if (smtp_tls_authorize_mx_hostname(state->tls, *cpp)) {
msg_warn("hostname '%s' is authorized" , *cpp);
match = 0 ;
}
}
smtp_state_free(state);
smtp_tls_policy_cache_flush();
return (match ? PASS : FAIL);
}
static int test_tls_reqd_no_sans_header(const struct TEST_CASE *tp)
{
SMTP_STATE *state = smtp_state_alloc();
const char *domain = "example.com" ;
const char *host = "mail.example.com" ;
const char *addr = "10.0.1.1" ;
int port = 25 ;
int want_level;
int ret = FAIL;
var_smtp_tls_level = "secure" ;
var_smtp_tls_policy = "static:none" ;
/* Test-dependent. */
state->request = &(DELIVER_REQUEST) {
.sendopts = 0
};
var_smtp_tls_wrappermode = 1 ;
var_tls_required_enable = 1 ;
want_level = TLS_LEV_NONE;
smtp_tls_list_init();
SMTP_ITER_INIT(state->iterator, domain, host, addr, port, state);
if (smtp_tls_policy_cache_query(state->why, state->tls,
state->iterator) == 0 ) {
msg_warn("smtp_tls_policy_cache_query failed: %s" ,
STR(state->why->reason));
} else if (state->tls->level != want_level) {
msg_warn("got TLS level '%s', want '%s'" ,
str_tls_level(state->tls->level), str_tls_level(want_level));
} else {
ret = PASS;
}
smtp_tls_policy_cache_flush();
smtp_state_free(state);
return (ret);
}
static int test_tls_reqd_no_with_wrappermode(const struct TEST_CASE *tp)
{
SMTP_STATE *state = smtp_state_alloc();
const char *domain = "example.com" ;
const char *host = "mail.example.com" ;
const char *addr = "10.0.1.1" ;
int port = 25 ;
int want_level;
int ret = FAIL;
var_smtp_tls_level = "secure" ;
var_smtp_tls_policy = "static:none" ;
/* Test-dependent. */
state->request = &(DELIVER_REQUEST) {
.sendopts = SOPT_REQUIRETLS_HEADER
};
var_smtp_tls_wrappermode = 1 ;
var_tls_required_enable = 1 ;
want_level = TLS_LEV_ENCRYPT;
smtp_tls_list_init();
SMTP_ITER_INIT(state->iterator, domain, host, addr, port, state);
if (smtp_tls_policy_cache_query(state->why, state->tls,
state->iterator) == 0 ) {
msg_warn("smtp_tls_policy_cache_query failed: %s" ,
STR(state->why->reason));
} else if (state->tls->level != want_level) {
msg_warn("got TLS level '%s', want '%s'" ,
str_tls_level(state->tls->level), str_tls_level(want_level));
} else {
ret = PASS;
}
smtp_tls_policy_cache_flush();
smtp_state_free(state);
return (ret);
}
static int test_tls_reqd_no_sans_wrappermode(const struct TEST_CASE *tp)
{
SMTP_STATE *state = smtp_state_alloc();
const char *domain = "example.com" ;
const char *host = "mail.example.com" ;
const char *addr = "10.0.1.1" ;
int port = 25 ;
int want_level;
int ret = FAIL;
var_smtp_tls_level = "secure" ;
var_smtp_tls_policy = "static:none" ;
/* Test-dependent. */
state->request = &(DELIVER_REQUEST) {
.sendopts = SOPT_REQUIRETLS_HEADER
};
var_smtp_tls_wrappermode = 0 ;
var_tls_required_enable = 1 ;
want_level = TLS_LEV_MAY;
smtp_tls_list_init();
SMTP_ITER_INIT(state->iterator, domain, host, addr, port, state);
if (smtp_tls_policy_cache_query(state->why, state->tls,
state->iterator) == 0 ) {
msg_warn("smtp_tls_policy_cache_query failed: %s" ,
STR(state->why->reason));
} else if (state->tls->level != want_level) {
msg_warn("got TLS level '%s', want '%s'" ,
str_tls_level(state->tls->level), str_tls_level(want_level));
} else {
ret = PASS;
}
smtp_tls_policy_cache_flush();
smtp_state_free(state);
return (ret);
}
static const struct TEST_CASE test_cases[] = {
{"sts_policy_smoke_test" , sts_policy_smoke_test,},
{"obs_sts_policy_smoke_test" , obs_sts_policy_smoke_test,},
{"test_hostname_authorization" , test_hostname_authorization},
{"test_tls_reqd_no_sans_header" , test_tls_reqd_no_sans_header},
{"test_tls_reqd_no_with_wrappermode" , test_tls_reqd_no_with_wrappermode},
{"test_tls_reqd_no_sans_wrappermode" , test_tls_reqd_no_sans_wrappermode},
{0 },
};
int main(int argc, char **argv)
{
static int tests_passed = 0 ;
static int tests_failed = 0 ;
const struct TEST_CASE *tp;
msg_vstream_init(sane_basename((VSTRING *) 0 , argv[0 ]), VSTREAM_ERR);
for (tp = test_cases; tp->label; tp++) {
msg_info("RUN %s" , tp->label);
test_setup();
if (tp->action(tp) == PASS) {
msg_info("PASS %s" , tp->label);
tests_passed += 1 ;
} else {
msg_info("FAIL %s" , tp->label);
tests_failed += 1 ;
}
test_teardown();
}
msg_info("PASS=%d FAIL=%d" , tests_passed, tests_failed);
exit (tests_failed != 0 );
}
#else
int main(int argc, char **argv)
{
msg_fatal("this program requires `#define USE_TLS'" );
}
#endif
Messung V0.5 in Prozent C=72 H=94 G=83
¤ Dauer der Verarbeitung: 0.25 Sekunden
(vorverarbeitet am 2026-08-08)
¤
*© Formatika GbR, Deutschland