From 22b2d17a6a86b8a61b4f76c05fb2c9c888670743 Mon Sep 17 00:00:00 2001 From: Wietse Z Venema Date: Mon, 6 Apr 2026 00:00:00 -0500 Subject: [PATCH] postfix-3.12-20260406 --- postfix/HISTORY | 30 ++- postfix/README_FILES/TLS_README | 246 ++++++++++++++++++++++ postfix/html/TLS_README.html | 273 +++++++++++++++++++++++++ postfix/html/postconf.5.html | 19 ++ postfix/man/man5/postconf.5 | 19 ++ postfix/proto/TLS_README.html | 273 +++++++++++++++++++++++++ postfix/proto/postconf.proto | 19 ++ postfix/proto/stop.double-history | 1 + postfix/proto/stop.double-proto-html | 2 + postfix/proto/stop.spell-proto-html | 39 ++++ postfix/src/global/Makefile.in | 10 +- postfix/src/global/dict_sqlite_test.c | 142 +++++-------- postfix/src/global/hfrom_format_test.c | 16 +- postfix/src/global/mail_version.h | 2 +- postfix/src/tls/tls.h | 4 +- postfix/src/tls/tls_client.c | 2 +- postfix/src/tls/tls_misc.c | 82 ++++---- postfix/src/tls/tls_server.c | 55 +---- 18 files changed, 1030 insertions(+), 204 deletions(-) diff --git a/postfix/HISTORY b/postfix/HISTORY index 7562757c6..d99f00d0e 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -30743,11 +30743,12 @@ Apologies for any names omitted. Cleanup: migrate the MySQL client from find_inet(3) to find_inet_service(3). Files: src/global/dict_mysql.c. - Feature: new NAME_MASK_NULL flag. When is present, the - str_name_mask_opt() function will output a string "0" when - the input mask is empty. Files: util/name_mask.[hc]. + Feature: new NAME_MASK_NULL flag. When this flag is set, + the str_name_mask_opt() function will output a string "0" + when the input mask is empty. Otherwise, the result is an + empty string. Files: util/name_mask.[hc]. - Cleanup: to make dict_open() easier to test, with support + Cleanup: make dict_open() easier to test, with support for graceful degradation after dictionary type or name syntax errors. File: util/dict_open.c. @@ -30804,6 +30805,27 @@ Apologies for any names omitted. smtpd/Makefile.in, smtpd/smtpd_check.c, util/dict_open.c, util/dynamicmaps.c, util/Makefile.in. +20260404 + + Refactored SNI support and eliminated some duplicate code + and data. Viktor Dukhovni. Files: tls/tls_client.c, + tls/tls.h, tls/tls_misc.c, tls/tls_server.c. + +20260405 + + Documentation: in TLS_README, step-by-step instructions to + test Postfix SMTP server SNI support, and how to avoid a + null pointer bug in unpatched OpenSSL versions 3.2..3.6; + this text is linked to from the postconf(5) manpage. Viktor + Dukhovni. Files: proto/postconf.proto, proto/TLS_README.html. + + Testing: In the SQLite test, call ptest_skip() if Postfix + is built without SQLite (after converting the test to use + Ptest). Files: global/Makefile.in, global/dict_sqlite_test.c. + + Testing: expect_ptest_error() should be expect_ptest_log_event(). + File: global/hfrom_format_test.c. + TODO Reorganize PTEST_LIB, PMOCK_LIB, TESTLIB, TESTLIBS, etc. diff --git a/postfix/README_FILES/TLS_README b/postfix/README_FILES/TLS_README index 9a75f24a9..59791412f 100644 --- a/postfix/README_FILES/TLS_README +++ b/postfix/README_FILES/TLS_README @@ -71,6 +71,7 @@ Topics covered in this section: * Server access control * Server-side cipher controls * Miscellaneous server controls + * Testing SNI support SSeerrvveerr--ssiiddee cceerrttiiffiiccaattee aanndd pprriivvaattee kkeeyy ccoonnffiigguurraattiioonn @@ -747,6 +748,251 @@ release may be enabled by other means in a later release, and the mask bit will then be ignored. Therefore, use of the hexadecimal mask is only a temporary measure until a new Postfix or OpenSSL release provides a better solution. +TTeessttiinngg SSNNII ssuuppppoorrtt + +The Postfix SMTP server supports dynamic selection of private keys and +certificate chains based on the content of the server-name-indication (SNI) TLS +extension sent by the SMTP client. See the documentation of the +tls_server_sni_maps configuration parameter for basic syntax details. + +Below you'll see how to test SNI support, and, in particular, how to determine +whether your OpenSSL runtime has been patched to correctly handle SNI with +recently added public key algorithms, such as the post-quantum MMLL--DDSSAA. Use of +post-quantum certificates via SNI will become relevant once certification +authorities start to issue post-quantum certificates. Until then, with self- +signed certificates, it rarely matters what names happen to appear in a +certificate that can't be chained to a trusted CA. + +First let's check that SNI support works with RSA keys. The below commands will +generate a key and self-signed certificate: + + # cfdir=$(postconf -dxh config_directory) + # cd "$cfdir" + # rchain="rsa-test.pem" + # fqdn=sni-test.localhost + # openssl genpkey -out "$rchain" -algorithm rsa \ + -pkeyopt rsa_keygen_bits:2048 + # openssl req -new -x509 -key "$rchain" -subj "/CN=$fqdn" \ + -addext "basicConstraints = critical,CA:false" \ + -addext "extendedKeyUsage = serverAuth" \ + -addext "subjectAltName = DNS:$fqdn" >> "$rchain" + +Next, we check that the private key matches the public key in the certificate: + + # cfdir=$(postconf -dxh config_directory) + # cd "$cfdir" + # rchain=rsa-test.pem + # kh=$(openssl pkey -in "$rchain" -pubout -outform DER | + openssl dgst -sha256 | sed 's/.*= *//') + # xh=$(openssl x509 -in "$rchain" -noout -pubkey | + openssl pkey -pubin -outform DER | + openssl dgst -sha256 | sed 's/.*= *//') + # if [[ "X$kh" == "X$xh" ]] then + echo "Equal private and certificate pubkey hashes: $kh" + else + echo "key does not match certificate in ${rchain}" >&2 + fi + +Next, we construct a test SNI table (as a matter of good habit, making sure it +is not world-readable), and check that queries return the same public key hash +as before: + + # cfdir=$(postconf -dxh config_directory) + # cd "$cfdir" + # rchain=rsa-test.pem + # fqdn=sni-test.localhost + # dbtype=$(postconf -xh default_database_type) + # snitab=sni-test-table + # ( + umask 077 + printf "%s\t%s\n" "$fqdn" "$cfdir/$rchain" > "$snitab" + ) + # postmap -F "${dbtype}:${snitab}" + # postmap -Fq "$fqdn" "${dbtype}:${snitab}" | + openssl pkey -pubout -outform DER | + openssl dgst -sha256 | sed 's/.*= */pubkey hash: /' + # postmap -Fq "$fqdn" "${dbtype}:${snitab}" | + openssl x509 -noout -pubkey | + openssl pkey -pubin -outform DER | + openssl dgst -sha256 | sed 's/.*= */pubkey hash: /' + +Next, we add a test SNI-enabled SMTP service on an unused port and loopback IP +address: + + # cfdir=$(postconf -dxh config_directory) + # cd "$cfdir" + # rchain=rsa-test.pem + # fqdn=sni-test.localhost + # dbtype=$(postconf -xh default_database_type) + # snitab=sni-test-table + # port=$(perl -MIO::Socket -le ' + # Just pick a "random" unused port if you do not have Perl + print IO::Socket::INET->new( + Listen => 1, + ReuseAddr => 1, + LocalHost => "127.0.0.1", + LocalPort => 0)->sockport() + ') + # postconf -M "$( + printf '%s/inet = %s inet n - n - - smtpd' \ + "127.0.0.1:$port" "127.0.0.1:$port" + printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \ + "$dbtype" "$cfdir" "$snitab" + printf '\t-o { syslog_name = postfix/sni-test }' + )" + # postfix reload; sleep 1 + # + # kh=$(openssl pkey -in "$rchain" -pubout -outform DER | + openssl dgst -sha256 | sed 's/.*= *//') + # ( sleep 1; printf "QUIT\r\n") | + openssl s_client -brief -starttls smtp \ + -connect "127.0.0.1:$port" \ + -verify 1 -verify_return_error \ + -dane_tlsa_domain "$fqdn" \ + -dane_tlsa_rrdata "3 1 1 $kh" && + printf "OK: Got expected SNI chain\n" || + printf "ERROR: Unexpected SNI certificate\n" >&2 + # postconf -MX "127.0.0.1:$port/inet" + # postfix reload + +The above basic RSA tests are expected to work, if the instructions are +followed accurately. If there's an unexpected failure, and the reasons are +unclear, see the debugging tutorial, for how seek help from the ppoossttffiixx--uusseerrss +list. + +With the basic RSA test out of the way, let's try combining RSA and ECDSA keys: + + # cfdir=$(postconf -dxh config_directory) + # cd "$cfdir" + # rchain="rsa-test.pem" + # echain="ec-test.pem" + # rsasig="rsa_pss_rsae_sha256:rsa_pss_rsae_sha384:rsa_pss_rsae_sha512" + # rsasig="$rsasig:rsa_pss_pss_sha256:rsa_pss_pss_sha384: + rsa_pss_pss_sha512" + # rsasig="$rsasig:rsa_pkcs1_sha256:rsa_pkcs1_sha384:rsa_pkcs1_sha512" + # ecsig="ecdsa_secp256r1_sha256:ecdsa_secp384r1_sha384: + ecdsa_secp521r1_sha512" + # fqdn=sni-test.localhost + # dbtype=$(postconf -xh default_database_type) + # snitab=sni-test-table + # openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 \ + -out "$rchain" + # openssl req -new -x509 -key "$rchain" -subj "/CN=$fqdn" \ + -addext "basicConstraints = critical,CA:false" \ + -addext "extendedKeyUsage = serverAuth" \ + -addext "subjectAltName = DNS:$fqdn" >> "$rchain" + # openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -out "$echain" + # openssl req -new -x509 -key "$echain" -subj "/CN=$fqdn" \ + -addext "basicConstraints = critical,CA:false" \ + -addext "extendedKeyUsage = serverAuth" \ + -addext "subjectAltName = DNS:$fqdn" >> "$echain" + # rh=$(openssl pkey -in "$rchain" -pubout -outform DER | + openssl dgst -sha256 | sed 's/.*= *//') + # eh=$(openssl pkey -in "$echain" -pubout -outform DER | + openssl dgst -sha256 | sed 's/.*= *//') + # ( + umask 077 + printf "%s\t%s, %s\n" \ + "$fqdn" "$cfdir/$rchain" "$cfdir/$echain" > "$snitab" + ) + # postmap -F "${dbtype}:${snitab}" + # port=$(perl -MIO::Socket -le ' + print IO::Socket::INET->new( + Listen => 1, + ReuseAddr => 1, + LocalHost => "127.0.0.1", + LocalPort => 0)->sockport() + ') + # postconf -M "$( + printf '%s/inet = %s inet n - n - - smtpd' \ + "127.0.0.1:$port" "127.0.0.1:$port" + printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \ + "$dbtype" "$cfdir" "$snitab" + printf '\t-o { syslog_name = postfix/sni-test }' + )" + # postfix reload; sleep 1 + # ( sleep 1; printf "QUIT\r\n") | + openssl s_client -brief -starttls smtp \ + -connect "127.0.0.1:$port" \ + -sigalgs "$rsasig" -cipher aRSA \ + -verify 1 -verify_return_error \ + -dane_tlsa_domain "$fqdn" \ + -dane_tlsa_rrdata "3 1 1 $rh" && + printf "OK: Got expected RSA SNI chain\n" || + printf "ERROR: Unexpected RSA server certificate\n" >&2 + # ( sleep 1; printf "QUIT\r\n") | + openssl s_client -brief -starttls smtp \ + -connect "127.0.0.1:$port" \ + -sigalgs "$ecsig" -cipher aECDSA \ + -verify 1 -verify_return_error \ + -dane_tlsa_domain "$fqdn" \ + -dane_tlsa_rrdata "3 1 1 $eh" && + printf "OK: Got expected EC SNI chain\n" || + printf "ERROR: Unexpected EC SNI certificate\n" >&2 + # postconf -MX "127.0.0.1:$port/inet" + # postfix reload + +With a dual algorithm RSA + ECDSA chain also successful (for most users just +single-algorithm RSA is the better choice), it is time to check whether the +OpenSSL runtime supports SNI also for post-quantum algorithm keys. This +requires OpenSSL 3.5 or later, earlier versions don't support the MMLL--DDSSAA +algorithm needed for this test: + + # cfdir=$(postconf -dxh config_directory) + # cd "$cfdir" + # mchain="test-mldsa-chain.pem" + # mlsig="mldsa44" + # fqdn=sni-test.localhost + # dbtype=$(postconf -xh default_database_type) + # snitab=sni-test-table + # openssl genpkey -algorithm ml-dsa-44 -out "$mchain" + # openssl req -new -x509 -key "$mchain" -subj "/CN=$fqdn" \ + -addext "basicConstraints = critical,CA:false" \ + -addext "extendedKeyUsage = serverAuth" \ + -addext "subjectAltName = DNS:$fqdn" >> "$mchain" + # mh=$(openssl pkey -in "$mchain" -pubout -outform DER | + openssl dgst -sha256 | sed 's/.*= *//') + # ( + umask 077 + printf "%s\t%s\n" "$fqdn" "$cfdir/$mchain" > "$snitab" + ) + # postmap -F "${dbtype}:${snitab}" + # port=$(perl -MIO::Socket -le ' + print IO::Socket::INET->new( + Listen => 1, + ReuseAddr => 1, + LocalHost => "127.0.0.1", + LocalPort => 0)->sockport() + ') + # postconf -M "$( + printf '%s/inet = %s inet n - n - - smtpd' \ + "127.0.0.1:$port" "127.0.0.1:$port" + printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \ + "$dbtype" "$cfdir" "$snitab" + printf '\t-o { syslog_name = postfix/sni-test }' + )" + # postfix reload; sleep 1 + # ( sleep 1; printf "QUIT\r\n") | + openssl s_client -brief -starttls smtp \ + -connect "127.0.0.1:$port" \ + -sigalgs "$mlsig" -tls1_3 \ + -verify 1 -verify_return_error \ + -dane_tlsa_domain "$fqdn" \ + -dane_tlsa_rrdata "3 1 1 $mh" && + printf "OK: Got expected ML-DSA SNI chain\n" || + printf "ERROR: Unexpected ML-DSA server certificate\n" >&2 + # postconf -MX "127.0.0.1:$port/inet" + # postfix reload + +If all went well, your OpenSSL runtime working SNI support for also for newer +public key algorithms, such as MMLL--DDSSAA. If the test failed, and the Postfix logs +show that the test Postfix SMTP server failed with a segfault, your OpenSSL +runtime has not yet been patched. The test is not viable with OpenSSL 3.4 or +earlier, which don't include MMLL--DDSSAA support, and is expected to work with +OpenSSL 4.0, which should be free of the underlying defect when initially +released. + SSMMTTPP CClliieenntt ssppeecciiffiicc sseettttiinnggss Topics covered in this section: diff --git a/postfix/html/TLS_README.html b/postfix/html/TLS_README.html index 4dc06dcb1..e5f8654dc 100644 --- a/postfix/html/TLS_README.html +++ b/postfix/html/TLS_README.html @@ -145,6 +145,8 @@ key configuration
  • Miscellaneous server controls +
  • Testing SNI support +

    Server-side certificate and private @@ -1038,6 +1040,277 @@ ignored. Therefore, use of the hexadecimal mask is only a temporary measure until a new Postfix or OpenSSL release provides a better solution.

    +

    Testing SNI support

    + +

    The Postfix SMTP server supports dynamic selection of private keys and +certificate chains based on the content of the server-name-indication (SNI) +TLS extension sent by the SMTP client. See the documentation of the tls_server_sni_maps +configuration parameter for basic syntax details.

    + +

    Below you'll see how to test SNI support, and, in particular, how to +determine whether your OpenSSL runtime has been patched to correctly handle +SNI with recently added public key algorithms, such as the post-quantum +ML-DSA. Use of post-quantum certificates via SNI will become +relevant once certification authorities start to issue post-quantum +certificates. Until then, with self-signed certificates, it rarely matters +what names happen to appear in a certificate that can't be chained to a +trusted CA.

    + +

    First let's check that SNI support works with RSA keys. The below +commands will generate a key and self-signed certificate:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain="rsa-test.pem"
    +    # fqdn=sni-test.localhost
    +    # openssl genpkey -out "$rchain" -algorithm rsa \
    +        -pkeyopt rsa_keygen_bits:2048
    +    # openssl req -new -x509 -key "$rchain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$rchain"
    +
    +
    + +

    Next, we check that the private key matches the public key in the +certificate:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain=rsa-test.pem
    +    # kh=$(openssl pkey -in "$rchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # xh=$(openssl x509 -in "$rchain" -noout -pubkey |
    +        openssl pkey -pubin -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # if [[ "X$kh" == "X$xh" ]] then
    +        echo "Equal private and certificate pubkey hashes: $kh"
    +      else
    +        echo "key does not match certificate in ${rchain}" >&2
    +      fi
    +
    +
    + +

    Next, we construct a test SNI table (as a matter of good habit, making +sure it is not world-readable), and check that queries return the same public +key hash as before:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain=rsa-test.pem
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # (
    +        umask 077
    +        printf "%s\t%s\n" "$fqdn" "$cfdir/$rchain" > "$snitab"
    +      )
    +    # postmap -F "${dbtype}:${snitab}"
    +    # postmap -Fq "$fqdn" "${dbtype}:${snitab}" |
    +        openssl pkey -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= */pubkey hash: /'
    +    # postmap -Fq "$fqdn" "${dbtype}:${snitab}" |
    +        openssl x509 -noout -pubkey |
    +        openssl pkey -pubin -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= */pubkey hash: /'
    +
    +
    + +

    Next, we add a test SNI-enabled SMTP service on an unused port and +loopback IP address:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain=rsa-test.pem
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # port=$(perl -MIO::Socket -le '
    +        # Just pick a "random" unused port if you do not have Perl
    +        print IO::Socket::INET->new(
    +            Listen => 1,
    +            ReuseAddr => 1,
    +            LocalHost => "127.0.0.1",
    +            LocalPort => 0)->sockport()
    +            ')
    +    # postconf -M "$(
    +        printf '%s/inet = %s inet n - n - - smtpd' \
    +            "127.0.0.1:$port" "127.0.0.1:$port"
    +        printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \
    +            "$dbtype" "$cfdir" "$snitab"
    +        printf '\t-o { syslog_name = postfix/sni-test }'
    +        )"
    +    # postfix reload; sleep 1
    +    #
    +    # kh=$(openssl pkey -in "$rchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $kh" &&
    +        printf "OK: Got expected SNI chain\n" ||
    +        printf "ERROR: Unexpected SNI certificate\n" >&2
    +    # postconf -MX "127.0.0.1:$port/inet"
    +    # postfix reload
    +
    +
    + +

    The above basic RSA tests are expected to work, if the instructions +are followed accurately. If there's an unexpected failure, and the reasons +are unclear, see the debugging +tutorial, for how seek help from the postfix-users list.

    + +

    With the basic RSA test out of the way, let's try combining RSA and +ECDSA keys:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain="rsa-test.pem"
    +    # echain="ec-test.pem"
    +    # rsasig="rsa_pss_rsae_sha256:rsa_pss_rsae_sha384:rsa_pss_rsae_sha512"
    +    # rsasig="$rsasig:rsa_pss_pss_sha256:rsa_pss_pss_sha384:rsa_pss_pss_sha512"
    +    # rsasig="$rsasig:rsa_pkcs1_sha256:rsa_pkcs1_sha384:rsa_pkcs1_sha512"
    +    # ecsig="ecdsa_secp256r1_sha256:ecdsa_secp384r1_sha384:ecdsa_secp521r1_sha512"
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 \
    +        -out "$rchain"
    +    # openssl req -new -x509 -key "$rchain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$rchain"
    +    # openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:prime256v1 \
    +        -out "$echain"
    +    # openssl req -new -x509 -key "$echain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$echain"
    +    # rh=$(openssl pkey -in "$rchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # eh=$(openssl pkey -in "$echain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # (
    +        umask 077
    +        printf "%s\t%s, %s\n" \
    +            "$fqdn" "$cfdir/$rchain" "$cfdir/$echain" > "$snitab"
    +      )
    +    # postmap -F "${dbtype}:${snitab}"
    +    # port=$(perl -MIO::Socket -le '
    +        print IO::Socket::INET->new(
    +            Listen => 1,
    +            ReuseAddr => 1,
    +            LocalHost => "127.0.0.1",
    +            LocalPort => 0)->sockport()
    +            ')
    +    # postconf -M "$(
    +        printf '%s/inet = %s inet n - n - - smtpd' \
    +            "127.0.0.1:$port" "127.0.0.1:$port"
    +        printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \
    +            "$dbtype" "$cfdir" "$snitab"
    +        printf '\t-o { syslog_name = postfix/sni-test }'
    +        )"
    +    # postfix reload; sleep 1
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -sigalgs "$rsasig" -cipher aRSA \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $rh" &&
    +        printf "OK: Got expected RSA SNI chain\n" ||
    +        printf "ERROR: Unexpected RSA server certificate\n" >&2
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -sigalgs "$ecsig" -cipher aECDSA \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $eh" &&
    +        printf "OK: Got expected EC SNI chain\n" ||
    +        printf "ERROR: Unexpected EC SNI certificate\n" >&2
    +    # postconf -MX "127.0.0.1:$port/inet"
    +    # postfix reload
    +
    +
    + +

    With a dual algorithm RSA + ECDSA chain also successful (for most users +just single-algorithm RSA is the better choice), it is time to check +whether the OpenSSL runtime supports SNI also for post-quantum algorithm +keys. This requires OpenSSL 3.5 or later, earlier versions don't support +the ML-DSA algorithm needed for this test:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # mchain="test-mldsa-chain.pem"
    +    # mlsig="mldsa44"
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # openssl genpkey -algorithm ml-dsa-44 -out "$mchain"
    +    # openssl req -new -x509 -key "$mchain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$mchain"
    +    # mh=$(openssl pkey -in "$mchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # (
    +        umask 077
    +        printf "%s\t%s\n" "$fqdn" "$cfdir/$mchain" > "$snitab"
    +      )
    +    # postmap -F "${dbtype}:${snitab}"
    +    # port=$(perl -MIO::Socket -le '
    +        print IO::Socket::INET->new(
    +            Listen => 1,
    +            ReuseAddr => 1,
    +            LocalHost => "127.0.0.1",
    +            LocalPort => 0)->sockport()
    +            ')
    +    # postconf -M "$(
    +        printf '%s/inet = %s inet n - n - - smtpd' \
    +            "127.0.0.1:$port" "127.0.0.1:$port"
    +        printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \
    +            "$dbtype" "$cfdir" "$snitab"
    +        printf '\t-o { syslog_name = postfix/sni-test }'
    +        )"
    +    # postfix reload; sleep 1
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -sigalgs "$mlsig" -tls1_3 \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $mh" &&
    +        printf "OK: Got expected ML-DSA SNI chain\n" ||
    +        printf "ERROR: Unexpected ML-DSA server certificate\n" >&2
    +    # postconf -MX "127.0.0.1:$port/inet"
    +    # postfix reload
    +
    +
    + +

    If all went well, your OpenSSL runtime working SNI support for also for +newer public key algorithms, such as ML-DSA. If the test failed, and +the Postfix logs show that the test Postfix SMTP server failed with a +segfault, your OpenSSL runtime has not yet been patched. The test is not +viable with OpenSSL 3.4 or earlier, which don't include ML-DSA +support, and is expected to work with OpenSSL 4.0, which should be free of +the underlying defect when initially released.

    +

    SMTP Client specific settings

    Topics covered in this section:

    diff --git a/postfix/html/postconf.5.html b/postfix/html/postconf.5.html index 771534b7e..70e3a587b 100644 --- a/postfix/html/postconf.5.html +++ b/postfix/html/postconf.5.html @@ -21883,6 +21883,25 @@ required in the TLS SNI extension).

    but here scoped to just TLS connections in which the client sends a matching SNI domain name.

    +

    Unpatched versions of OpenSSL versions 3.2 through 3.6, may not +correctly handle SNI certificate chains for private key algorithms +other than:

    + + + +

    The issue does not arise if you limit your SNI chains to just +these algorithms. Otherwise, inclusion of, e.g., ML-DSA +keys in SNI chains may lead to a null-pointer-exception (NPE). The +most recent update of your system's OpenSSL runtime libraries may +already include a fix for this issue. See the testing SNI section of the +Postfix TLS documentation for further guidance.

    +

    Example:

    diff --git a/postfix/man/man5/postconf.5 b/postfix/man/man5/postconf.5
    index 4d881b6ef..cf5256093 100644
    --- a/postfix/man/man5/postconf.5
    +++ b/postfix/man/man5/postconf.5
    @@ -15522,6 +15522,25 @@ smtp_tls_chain_files parameter (see there for additional details),
     but here scoped to just TLS connections in which the client sends
     a matching SNI domain name.
     .PP
    +Unpatched versions of OpenSSL versions 3.2 through 3.6, may not
    +correctly handle SNI certificate chains for private key algorithms
    +other than:
    +.IP \(bu
    +\fBRSA\fR
    +.IP \(bu
    +\fBECDSA\fR
    +.IP \(bu
    +\fBDSA\fR (obsolete and best avoided)
    +.IP \(bu
    +\fBEdDSA\fR (Ed25519 or Ed448).
    +.br
    +The issue does not arise if you limit your SNI chains to just
    +these algorithms. Otherwise, inclusion of, e.g., \fBML\-DSA\fR
    +keys in SNI chains may lead to a null\-pointer\-exception (NPE).  The
    +most recent update of your system's OpenSSL runtime libraries may
    +already include a fix for this issue. See the testing SNI section of the
    +Postfix TLS documentation for further guidance.
    +.PP
     Example:
     .sp
     .in +4
    diff --git a/postfix/proto/TLS_README.html b/postfix/proto/TLS_README.html
    index 879aae52e..fefe0e807 100644
    --- a/postfix/proto/TLS_README.html
    +++ b/postfix/proto/TLS_README.html
    @@ -145,6 +145,8 @@ key configuration 
     
     
  • Miscellaneous server controls +
  • Testing SNI support +

    Server-side certificate and private @@ -1038,6 +1040,277 @@ ignored. Therefore, use of the hexadecimal mask is only a temporary measure until a new Postfix or OpenSSL release provides a better solution.

    +

    Testing SNI support

    + +

    The Postfix SMTP server supports dynamic selection of private keys and +certificate chains based on the content of the server-name-indication (SNI) +TLS extension sent by the SMTP client. See the documentation of the tls_server_sni_maps +configuration parameter for basic syntax details.

    + +

    Below you'll see how to test SNI support, and, in particular, how to +determine whether your OpenSSL runtime has been patched to correctly handle +SNI with recently added public key algorithms, such as the post-quantum +ML-DSA. Use of post-quantum certificates via SNI will become +relevant once certification authorities start to issue post-quantum +certificates. Until then, with self-signed certificates, it rarely matters +what names happen to appear in a certificate that can't be chained to a +trusted CA.

    + +

    First let's check that SNI support works with RSA keys. The below +commands will generate a key and self-signed certificate:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain="rsa-test.pem"
    +    # fqdn=sni-test.localhost
    +    # openssl genpkey -out "$rchain" -algorithm rsa \
    +        -pkeyopt rsa_keygen_bits:2048
    +    # openssl req -new -x509 -key "$rchain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$rchain"
    +
    +
    + +

    Next, we check that the private key matches the public key in the +certificate:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain=rsa-test.pem
    +    # kh=$(openssl pkey -in "$rchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # xh=$(openssl x509 -in "$rchain" -noout -pubkey |
    +        openssl pkey -pubin -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # if [[ "X$kh" == "X$xh" ]] then
    +        echo "Equal private and certificate pubkey hashes: $kh"
    +      else
    +        echo "key does not match certificate in ${rchain}" >&2
    +      fi
    +
    +
    + +

    Next, we construct a test SNI table (as a matter of good habit, making +sure it is not world-readable), and check that queries return the same public +key hash as before:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain=rsa-test.pem
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # (
    +        umask 077
    +        printf "%s\t%s\n" "$fqdn" "$cfdir/$rchain" > "$snitab"
    +      )
    +    # postmap -F "${dbtype}:${snitab}"
    +    # postmap -Fq "$fqdn" "${dbtype}:${snitab}" |
    +        openssl pkey -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= */pubkey hash: /'
    +    # postmap -Fq "$fqdn" "${dbtype}:${snitab}" |
    +        openssl x509 -noout -pubkey |
    +        openssl pkey -pubin -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= */pubkey hash: /'
    +
    +
    + +

    Next, we add a test SNI-enabled SMTP service on an unused port and +loopback IP address:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain=rsa-test.pem
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # port=$(perl -MIO::Socket -le '
    +        # Just pick a "random" unused port if you do not have Perl
    +        print IO::Socket::INET->new(
    +            Listen => 1,
    +            ReuseAddr => 1,
    +            LocalHost => "127.0.0.1",
    +            LocalPort => 0)->sockport()
    +            ')
    +    # postconf -M "$(
    +        printf '%s/inet = %s inet n - n - - smtpd' \
    +            "127.0.0.1:$port" "127.0.0.1:$port"
    +        printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \
    +            "$dbtype" "$cfdir" "$snitab"
    +        printf '\t-o { syslog_name = postfix/sni-test }'
    +        )"
    +    # postfix reload; sleep 1
    +    #
    +    # kh=$(openssl pkey -in "$rchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $kh" &&
    +        printf "OK: Got expected SNI chain\n" ||
    +        printf "ERROR: Unexpected SNI certificate\n" >&2
    +    # postconf -MX "127.0.0.1:$port/inet"
    +    # postfix reload
    +
    +
    + +

    The above basic RSA tests are expected to work, if the instructions +are followed accurately. If there's an unexpected failure, and the reasons +are unclear, see the debugging +tutorial, for how seek help from the postfix-users list.

    + +

    With the basic RSA test out of the way, let's try combining RSA and +ECDSA keys:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # rchain="rsa-test.pem"
    +    # echain="ec-test.pem"
    +    # rsasig="rsa_pss_rsae_sha256:rsa_pss_rsae_sha384:rsa_pss_rsae_sha512"
    +    # rsasig="$rsasig:rsa_pss_pss_sha256:rsa_pss_pss_sha384:rsa_pss_pss_sha512"
    +    # rsasig="$rsasig:rsa_pkcs1_sha256:rsa_pkcs1_sha384:rsa_pkcs1_sha512"
    +    # ecsig="ecdsa_secp256r1_sha256:ecdsa_secp384r1_sha384:ecdsa_secp521r1_sha512"
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 \
    +        -out "$rchain"
    +    # openssl req -new -x509 -key "$rchain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$rchain"
    +    # openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:prime256v1 \
    +        -out "$echain"
    +    # openssl req -new -x509 -key "$echain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$echain"
    +    # rh=$(openssl pkey -in "$rchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # eh=$(openssl pkey -in "$echain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # (
    +        umask 077
    +        printf "%s\t%s, %s\n" \
    +            "$fqdn" "$cfdir/$rchain" "$cfdir/$echain" > "$snitab"
    +      )
    +    # postmap -F "${dbtype}:${snitab}"
    +    # port=$(perl -MIO::Socket -le '
    +        print IO::Socket::INET->new(
    +            Listen => 1,
    +            ReuseAddr => 1,
    +            LocalHost => "127.0.0.1",
    +            LocalPort => 0)->sockport()
    +            ')
    +    # postconf -M "$(
    +        printf '%s/inet = %s inet n - n - - smtpd' \
    +            "127.0.0.1:$port" "127.0.0.1:$port"
    +        printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \
    +            "$dbtype" "$cfdir" "$snitab"
    +        printf '\t-o { syslog_name = postfix/sni-test }'
    +        )"
    +    # postfix reload; sleep 1
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -sigalgs "$rsasig" -cipher aRSA \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $rh" &&
    +        printf "OK: Got expected RSA SNI chain\n" ||
    +        printf "ERROR: Unexpected RSA server certificate\n" >&2
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -sigalgs "$ecsig" -cipher aECDSA \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $eh" &&
    +        printf "OK: Got expected EC SNI chain\n" ||
    +        printf "ERROR: Unexpected EC SNI certificate\n" >&2
    +    # postconf -MX "127.0.0.1:$port/inet"
    +    # postfix reload
    +
    +
    + +

    With a dual algorithm RSA + ECDSA chain also successful (for most users +just single-algorithm RSA is the better choice), it is time to check +whether the OpenSSL runtime supports SNI also for post-quantum algorithm +keys. This requires OpenSSL 3.5 or later, earlier versions don't support +the ML-DSA algorithm needed for this test:

    + +
    +
    +    # cfdir=$(postconf -dxh config_directory)
    +    # cd "$cfdir"
    +    # mchain="test-mldsa-chain.pem"
    +    # mlsig="mldsa44"
    +    # fqdn=sni-test.localhost
    +    # dbtype=$(postconf -xh default_database_type)
    +    # snitab=sni-test-table
    +    # openssl genpkey -algorithm ml-dsa-44 -out "$mchain"
    +    # openssl req -new -x509 -key "$mchain" -subj "/CN=$fqdn" \
    +        -addext "basicConstraints = critical,CA:false" \
    +        -addext "extendedKeyUsage = serverAuth" \
    +        -addext "subjectAltName = DNS:$fqdn" >> "$mchain"
    +    # mh=$(openssl pkey -in "$mchain" -pubout -outform DER |
    +        openssl dgst -sha256 | sed 's/.*= *//')
    +    # (
    +        umask 077
    +        printf "%s\t%s\n" "$fqdn" "$cfdir/$mchain" > "$snitab"
    +      )
    +    # postmap -F "${dbtype}:${snitab}"
    +    # port=$(perl -MIO::Socket -le '
    +        print IO::Socket::INET->new(
    +            Listen => 1,
    +            ReuseAddr => 1,
    +            LocalHost => "127.0.0.1",
    +            LocalPort => 0)->sockport()
    +            ')
    +    # postconf -M "$(
    +        printf '%s/inet = %s inet n - n - - smtpd' \
    +            "127.0.0.1:$port" "127.0.0.1:$port"
    +        printf '\t-o { tls_server_sni_maps = %s:%s/%s }' \
    +            "$dbtype" "$cfdir" "$snitab"
    +        printf '\t-o { syslog_name = postfix/sni-test }'
    +        )"
    +    # postfix reload; sleep 1
    +    # ( sleep 1; printf "QUIT\r\n") |
    +        openssl s_client -brief -starttls smtp \
    +            -connect "127.0.0.1:$port" \
    +            -sigalgs "$mlsig" -tls1_3 \
    +            -verify 1 -verify_return_error \
    +            -dane_tlsa_domain "$fqdn" \
    +            -dane_tlsa_rrdata "3 1 1 $mh" &&
    +        printf "OK: Got expected ML-DSA SNI chain\n" ||
    +        printf "ERROR: Unexpected ML-DSA server certificate\n" >&2
    +    # postconf -MX "127.0.0.1:$port/inet"
    +    # postfix reload
    +
    +
    + +

    If all went well, your OpenSSL runtime working SNI support for also for +newer public key algorithms, such as ML-DSA. If the test failed, and +the Postfix logs show that the test Postfix SMTP server failed with a +segfault, your OpenSSL runtime has not yet been patched. The test is not +viable with OpenSSL 3.4 or earlier, which don't include ML-DSA +support, and is expected to work with OpenSSL 4.0, which should be free of +the underlying defect when initially released.

    +

    SMTP Client specific settings

    Topics covered in this section:

    diff --git a/postfix/proto/postconf.proto b/postfix/proto/postconf.proto index 8f7567434..253fc6ebc 100644 --- a/postfix/proto/postconf.proto +++ b/postfix/proto/postconf.proto @@ -18639,6 +18639,25 @@ smtp_tls_chain_files parameter (see there for additional details), but here scoped to just TLS connections in which the client sends a matching SNI domain name.

    +

    Unpatched versions of OpenSSL versions 3.2 through 3.6, may not +correctly handle SNI certificate chains for private key algorithms +other than:

    + +
      +
    • RSA +
    • ECDSA +
    • DSA (obsolete and best avoided) +
    • EdDSA (Ed25519 or Ed448). +
    + +

    The issue does not arise if you limit your SNI chains to just +these algorithms. Otherwise, inclusion of, e.g., ML-DSA +keys in SNI chains may lead to a null-pointer-exception (NPE). The +most recent update of your system's OpenSSL runtime libraries may +already include a fix for this issue. See the testing SNI section of the +Postfix TLS documentation for further guidance.

    +

    Example:

    diff --git a/postfix/proto/stop.double-history b/postfix/proto/stop.double-history
    index ab1eb020b..522a03470 100644
    --- a/postfix/proto/stop.double-history
    +++ b/postfix/proto/stop.double-history
    @@ -230,3 +230,4 @@ proto  proto stop proto stop double cc
      tls tls h tls tls_misc c tls tls_verify c 
      differ Files postalias postalias c local alias c 
      local local hc local Makefile 
    + Dukhovni Files proto postconf proto proto TLS_README html 
    diff --git a/postfix/proto/stop.double-proto-html b/postfix/proto/stop.double-proto-html
    index 25c2fad2e..90491409b 100644
    --- a/postfix/proto/stop.double-proto-html
    +++ b/postfix/proto/stop.double-proto-html
    @@ -390,3 +390,5 @@ mynetworks mynetworks lmdb etc postfix network_table
      void ptest_defer PTEST_CTX t void defer_fn void void defer_ctx 
     130 type_name unionmap static one fail fail 
     187 for cpp tp want_log cpp tp want_log MAX_WANT_LOG cpp cpp 
    + rsasig rsasig rsa_pss_pss_sha256 rsa_pss_pss_sha384 rsa_pss_pss_sha512 
    + rsasig rsasig rsa_pkcs1_sha256 rsa_pkcs1_sha384 rsa_pkcs1_sha512 
    diff --git a/postfix/proto/stop.spell-proto-html b/postfix/proto/stop.spell-proto-html
    index caaa74a7f..cc4ab72b5 100644
    --- a/postfix/proto/stop.spell-proto-html
    +++ b/postfix/proto/stop.spell-proto-html
    @@ -448,3 +448,42 @@ ccerts
     clientAuth
     notfound
     serverAuth
    +EdDSA
    +LocalHost
    +LocalPort
    +MIO
    +NPE
    +ReuseAddr
    +Unpatched
    +aECDSA
    +addext
    +basicConstraints
    +cfdir
    +dbtype
    +dxh
    +ec
    +echain
    +ecsig
    +extendedKeyUsage
    +genpkey
    +keygen
    +kh
    +mchain
    +mh
    +mldsa
    +mlsig
    +paramgen
    +pkcs
    +pkeyopt
    +pss
    +rchain
    +rh
    +rrdata
    +rsae
    +rsasig
    +sed
    +sigalgs
    +snitab
    +sockport
    +subjectAltName
    +xh
    diff --git a/postfix/src/global/Makefile.in b/postfix/src/global/Makefile.in
    index 273e7ab67..176b0c5d4 100644
    --- a/postfix/src/global/Makefile.in
    +++ b/postfix/src/global/Makefile.in
    @@ -403,8 +403,8 @@ ascii_header_text: ascii_header_text.c $(LIB) $(LIBS)
     sendopts_test: sendopts_test.o $(LIB) $(LIBS)
     	$(CC) $(CFLAGS) -o $@ $@.c $(LIB) $(LIBS) $(SYSLIBS)
     
    -dict_sqlite_test: dict_sqlite_test.o dict_sqlite.o $(LIB) $(LIBS)
    -	$(CC) $(CFLAGS) -o $@ $@.o dict_sqlite.o $(LIB) $(LIBS) \
    +dict_sqlite_test: dict_sqlite_test.o dict_sqlite.o $(PTEST_LIB) $(LIB) $(LIBS)
    +	$(CC) $(CFLAGS) -o $@ $@.o dict_sqlite.o $(PTEST_LIB) $(LIB) $(LIBS) \
     	    $(SYSLIBS) $(AUXLIBS_SQLITE)
     
     pol_stats_test: pol_stats_test.o pol_stats.o $(LIB) $(LIBS)
    @@ -1442,8 +1442,14 @@ dict_sqlite_test.o: ../../include/argv.h
     dict_sqlite_test.o: ../../include/check_arg.h
     dict_sqlite_test.o: ../../include/dict.h
     dict_sqlite_test.o: ../../include/msg.h
    +dict_sqlite_test.o: ../../include/msg_jmp.h
    +dict_sqlite_test.o: ../../include/msg_output.h
     dict_sqlite_test.o: ../../include/msg_vstream.h
     dict_sqlite_test.o: ../../include/myflock.h
    +dict_sqlite_test.o: ../../include/myrand.h
    +dict_sqlite_test.o: ../../include/pmock_expect.h
    +dict_sqlite_test.o: ../../include/ptest.h
    +dict_sqlite_test.o: ../../include/ptest_main.h
     dict_sqlite_test.o: ../../include/stringops.h
     dict_sqlite_test.o: ../../include/sys_defs.h
     dict_sqlite_test.o: ../../include/vbuf.h
    diff --git a/postfix/src/global/dict_sqlite_test.c b/postfix/src/global/dict_sqlite_test.c
    index b546300f9..0eae17cd9 100644
    --- a/postfix/src/global/dict_sqlite_test.c
    +++ b/postfix/src/global/dict_sqlite_test.c
    @@ -9,7 +9,9 @@
     /*	dict_sqlite_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.
    -/*
    +/* HERMETICITY
    +/* .ad
    +/* .fi
     /*	Each test creates a temporary test database and a corresponding
     /*	Postfix sqlite client configuration file, both having unique
     /*	names. Otherwise, each test is hermetic.
    @@ -18,7 +20,8 @@
     /* .fi
     /*	The Secure Mailer license must be distributed with this software.
     /* AUTHOR(S)
    -/*	Wietse Venema porcupine.org
    +/*	Wietse Venema
    +/*	porcupine.org
     /*--*/
     
      /*
    @@ -41,19 +44,11 @@
     #include 
     
      /*
    -  * TODO(wietse) make this a proper VSTREAM interface or test helper API.
    +  * Test libraries.
       */
    +#include 
     
    -/* vstream_swap - capture output for testing */
    -
    -static void vstream_swap(VSTREAM *one, VSTREAM *two)
    -{
    -    VSTREAM save;
    -
    -    save = *one;
    -    *one = *two;
    -    *two = save;
    -}
    +#ifdef HAS_SQLITE
     
      /*
       * Override the printable.c module because it may break some tests.
    @@ -74,7 +69,8 @@ char   *printable_except(char *string, int replacement, const char *except)
     
     /* create_and_populate_db - create an empty database and optionally populate */
     
    -static void create_and_populate_db(char *dbpath, const char *commands)
    +static void create_and_populate_db(PTEST_CTX *t, char *dbpath,
    +				           const char *commands)
     {
         int     fd;
     
    @@ -83,22 +79,23 @@ static void create_and_populate_db(char *dbpath, const char *commands)
          * adversary cannot rename or remove the file.
          */
         if ((fd = mkstemp(dbpath)) < 0)
    -	msg_fatal("mkstemp(\"%s\"): %m", dbpath);
    +	ptest_fatal(t, "mkstemp(\"%s\"): %m", dbpath);
         if (close(fd) < 0)
    -	msg_fatal("close %s: %m", dbpath);
    +	ptest_fatal(t, "close %s: %m", dbpath);
     
         /*
          * TODO(wietse) Open the database file, prepare and execute commands to
          * populate the database, and close the database.
          */
         if (commands) {
    -	msg_fatal("commands are not yet supported");
    +	ptest_fatal(t, "commands are not yet supported");
         }
     }
     
     /* create_and_populate_cf - create sqlite_table(5) configuration file */
     
    -static void create_and_populate_cf(char *cfpath, const char *dbpath,
    +static void create_and_populate_cf(PTEST_CTX *t, char *cfpath,
    +				           const char *dbpath,
     				           const char *cftext)
     {
         int     fd;
    @@ -109,24 +106,26 @@ static void create_and_populate_cf(char *cfpath, const char *dbpath,
          * Assume that an adversary cannot rename or remove the file.
          */
         if ((fd = mkstemp(cfpath)) < 0)
    -	msg_fatal("mkstemp(\"%s\"): %m", cfpath);
    +	ptest_fatal(t, "mkstemp(\"%s\"): %m", cfpath);
         if ((fp = vstream_fdopen(fd, O_WRONLY)) == 0)
    -	msg_fatal("vstream_fdopen: %m");
    +	ptest_fatal(t, "vstream_fdopen: %m");
         (void) vstream_fprintf(fp, "%s\ndbpath = %s\n", cftext, dbpath);
         if (vstream_fclose(fp) != 0)
    -	msg_fatal("vstream_fdclose: %m");
    +	ptest_fatal(t, "vstream_fdclose: %m");
     }
     
    +#endif					/* HAS_SQLITE */
    +
      /*
       * Test structure. Some tests may come their own.
       */
    -typedef struct TEST_CASE {
    -    const char *label;
    -    int     (*action) (const struct TEST_CASE *);
    +typedef struct PTEST_CASE {
    +    const char *testname;
    +    void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
         const char *commands;		/* commands or null */
         const char *settings;		/* sqlite_table(5) */
    -    const char *exp_warning;		/* substring match or null */
    -} TEST_CASE;
    +    const char *want_log;		/* substring match or null */
    +} PTEST_CASE;
     
     #define PASS    (0)
     #define FAIL    (1)
    @@ -135,112 +134,67 @@ typedef struct TEST_CASE {
     
     /* test_flag_non_recommended_query - flag non-recommended query payloads */
     
    -static int test_flag_non_recommended_query(const TEST_CASE *tp)
    +static void test_flag_non_recommended_query(PTEST_CTX *t, const PTEST_CASE *tp)
     {
    -    static VSTRING *msg_buf;
    -    VSTREAM *memory_stream;
    +#ifdef HAS_SQLITE
         const char template[] = PATH_TEMPLATE;
         char    dbpath[sizeof(template)];
         char    cfpath[sizeof(template)];
         DICT   *dict;
     
    -    if (msg_buf == 0)
    -	msg_buf = vstring_alloc(100);
    -
    -    /* Prepare scaffolding database and configuration files. */
    +    /* Prepare scaffolding database and configuration file. */
         memcpy(dbpath, template, sizeof(dbpath));
    -    create_and_populate_db(dbpath, tp->commands);
    +    create_and_populate_db(t, dbpath, tp->commands);
         memcpy(cfpath, template, sizeof(cfpath));
    -    create_and_populate_cf(cfpath, dbpath, tp->settings);
    -
    -    /* Run the test with custom STDERR stream. */
    -    VSTRING_RESET(msg_buf);
    -    VSTRING_TERMINATE(msg_buf);
    -    if ((memory_stream = vstream_memopen(msg_buf, O_WRONLY)) == 0)
    -	msg_fatal("open memory stream: %m");
    -    vstream_swap(VSTREAM_ERR, memory_stream);
    -    if ((dict = dict_sqlite_open(cfpath, O_RDONLY, DICT_FLAG_UTF8_REQUEST)) != 0)
    -	dict_close(dict);
    -    vstream_swap(memory_stream, VSTREAM_ERR);
    -    if (vstream_fclose(memory_stream))
    -	msg_fatal("close memory stream: %m");
    +    create_and_populate_cf(t, cfpath, dbpath, tp->settings);
    +
    +    if (tp->want_log)
    +	expect_ptest_log_event(t, tp->want_log);
    +    dict = dict_sqlite_open(cfpath, O_RDONLY, DICT_FLAG_UTF8_REQUEST);
    +    dict_close(dict);
     
         /* Cleanup scaffolding database and configuration files. */
         if (unlink(dbpath) < 0)
    -	msg_fatal("unlink %s: %m", dbpath);
    +	ptest_error(t, "unlink %s: %m", dbpath);
         if (unlink(cfpath) < 0)
    -	msg_fatal("unlink %s: %m", cfpath);
    -
    -    /* Verify the results. */
    -    if (tp->exp_warning == 0 && VSTRING_LEN(msg_buf) > 0) {
    -	msg_warn("got warning ``%s'', want ``null''", vstring_str(msg_buf));
    -	return (FAIL);
    -    }
    -    if (tp->exp_warning != 0
    -	&& strstr(vstring_str(msg_buf), tp->exp_warning) == 0) {
    -	msg_warn("got warning ``%s'', want ``%s''",
    -		 vstring_str(msg_buf), tp->exp_warning);
    -	return (FAIL);
    -    }
    -    return (PASS);
    +	ptest_error(t, "unlink %s: %m", cfpath);
    +#else
    +    ptest_skip(t);
    +#endif
     }
     
      /*
       * The list of test cases.
       */
    -static const TEST_CASE test_cases[] = {
    +static const PTEST_CASE ptestcases[] = {
     
         /*
          * Tests to flag non-recommended query forms. These create an empty test
          * database, and open it with the dict_sqlite client without querying it.
          */
    -    {.label = "no_dynamic_payload",
    +    {.testname = "no_dynamic_payload",
     	.action = test_flag_non_recommended_query,
     	.settings = "query = select a from b where c = 5",
         },
    -    {.label = "dynamic_payload_inside_recommended_quotes",
    +    {.testname = "dynamic_payload_inside_recommended_quotes",
     	.action = test_flag_non_recommended_query,
     	.settings = "query = select a from b where c = 'xx%syy'",
         },
    -    {.label = "dynamic_payload_without_quotes",
    +    {.testname = "dynamic_payload_without_quotes",
     	.action = test_flag_non_recommended_query,
     	.settings = "query = select s from b where c = xx%syy",
    -	.exp_warning = "contains >%s< without the recommended '' quotes",
    +	.want_log = "contains >%s< without the recommended '' quotes",
         },
    -    {.label = "payload_inside_double_quotes",
    +    {.testname = "payload_inside_double_quotes",
     	.action = test_flag_non_recommended_query,
     	.settings = "query = select s from b where c = \"xx%syy\"",
    -	.exp_warning = "contains >%s< without the recommended '' quotes",
    +	.want_log = "contains >%s< without the recommended '' quotes",
         },
     
         /*
          * TODO: Tests that actually populate a test database, and that query it
          * with the dict_sqlite client.
          */
    -    {0},
     };
     
    -int     main(int argc, char **argv)
    -{
    -    const TEST_CASE *tp;
    -    int     pass = 0;
    -    int     fail = 0;
    -
    -    msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR);
    -
    -    for (tp = test_cases; tp->label != 0; tp++) {
    -	int     test_failed;
    -
    -	msg_info("RUN  %s", tp->label);
    -	test_failed = tp->action(tp);
    -	if (test_failed) {
    -	    msg_info("FAIL %s", tp->label);
    -	    fail++;
    -	} else {
    -	    msg_info("PASS %s", tp->label);
    -	    pass++;
    -	}
    -    }
    -    msg_info("PASS=%d FAIL=%d", pass, fail);
    -    exit(fail != 0);
    -}
    +#include 
    diff --git a/postfix/src/global/hfrom_format_test.c b/postfix/src/global/hfrom_format_test.c
    index 9a6fe2e18..f5d1a3903 100644
    --- a/postfix/src/global/hfrom_format_test.c
    +++ b/postfix/src/global/hfrom_format_test.c
    @@ -31,7 +31,7 @@ typedef struct PTEST_CASE {
     struct name_test_case {
         const char *label;			/* identifies test case */
         const char *config;			/* configuration under test */
    -    const char *want_warning;		/* expected warning or empty */
    +    const char *want_log;		/* expected warning or empty */
         const int want_code;		/* expected code */
     };
     
    @@ -67,10 +67,10 @@ static void test_hfrom_format_parse(PTEST_CTX *t, const PTEST_CASE *tp)
     	PTEST_RUN(t, np->label, {
     	    int     got_code;
     
    -	    if (*np->want_warning)
    -		expect_ptest_error(t, np->want_warning);
    +	    if (*np->want_log)
    +		expect_ptest_log_event(t, np->want_log);
     	    got_code = hfrom_format_parse(np->label, np->config);
    -	    if (*np->want_warning == 0) {
    +	    if (*np->want_log == 0) {
     		if (got_code != np->want_code) {
     		    ptest_error(t, "got code \"%d\", want \"%d\"(%s)",
     			       got_code, np->want_code,
    @@ -84,7 +84,7 @@ static void test_hfrom_format_parse(PTEST_CTX *t, const PTEST_CASE *tp)
     struct code_test_case {
         const char *label;			/* identifies test case */
         int     code;			/* code under test */
    -    const char *want_warning;		/* expected warning or empty */
    +    const char *want_log;		/* expected warning or empty */
         const char *want_name;		/* expected namme */
     };
     
    @@ -115,10 +115,10 @@ static void test_str_hfrom_format_code(PTEST_CTX *t, const PTEST_CASE *tp)
     	PTEST_RUN(t, cp->label, {
     	    const char *got_name;
     
    -	    if (*cp->want_warning)
    -		expect_ptest_error(t, cp->want_warning);
    +	    if (*cp->want_log)
    +		expect_ptest_log_event(t, cp->want_log);
     	    got_name = str_hfrom_format_code(cp->code);
    -	    if (*cp->want_warning == 0) {
    +	    if (*cp->want_log == 0) {
     		if (strcmp(got_name, cp->want_name) != 0) {
     		    ptest_error(t, "got name: \"%s\", want: \"%s\"",
     			       got_name, cp->want_name);
    diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h
    index 2603ec1eb..79b686472 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	"20260403"
    +#define MAIL_RELEASE_DATE	"20260406"
     #define MAIL_VERSION_NUMBER	"3.12"
     
     #ifdef SNAPSHOT
    diff --git a/postfix/src/tls/tls.h b/postfix/src/tls/tls.h
    index 3326ffe36..971f1f2b1 100644
    --- a/postfix/src/tls/tls.h
    +++ b/postfix/src/tls/tls.h
    @@ -336,7 +336,6 @@ extern int tls_log_mask(const char *, const char *);
       */
     struct TLS_APPL_STATE {
         SSL_CTX *ssl_ctx;
    -    SSL_CTX *sni_ctx;
         int     log_mask;
         char   *cache_type;
     };
    @@ -714,13 +713,14 @@ extern int tls_set_my_certificate_key_info(SSL_CTX *, /* All */ const char *,
       */
     extern int TLScontext_index;
     
    -extern TLS_APPL_STATE *tls_alloc_app_context(SSL_CTX *, SSL_CTX *, int);
    +extern TLS_APPL_STATE *tls_alloc_app_context(SSL_CTX *, int);
     extern TLS_SESS_STATE *tls_alloc_sess_context(int, const char *);
     extern void tls_free_context(TLS_SESS_STATE *);
     extern void tls_check_version(void);
     extern long tls_bug_bits(void);
     extern void tls_print_errors(void);
     extern void tls_info_callback(const SSL *, int, int);
    +extern int tls_cert_cb(SSL *, void *);
     
     #if OPENSSL_VERSION_PREREQ(3,0)
     extern long tls_bio_dump_cb(BIO *, int, const char *, size_t, int, long,
    diff --git a/postfix/src/tls/tls_client.c b/postfix/src/tls/tls_client.c
    index 1fad4d7e5..e7ec3c8e8 100644
    --- a/postfix/src/tls/tls_client.c
    +++ b/postfix/src/tls/tls_client.c
    @@ -918,7 +918,7 @@ TLS_APPL_STATE *tls_client_init(const TLS_CLIENT_INIT_PROPS *props)
          * Allocate an application context, and populate with mandatory protocol
          * and cipher data.
          */
    -    app_ctx = tls_alloc_app_context(client_ctx, 0, log_mask);
    +    app_ctx = tls_alloc_app_context(client_ctx, log_mask);
     
         /*
          * The external session cache is implemented by the tlsmgr(8) process.
    diff --git a/postfix/src/tls/tls_misc.c b/postfix/src/tls/tls_misc.c
    index c6403fc9e..af234a74e 100644
    --- a/postfix/src/tls/tls_misc.c
    +++ b/postfix/src/tls/tls_misc.c
    @@ -845,18 +845,50 @@ void    tls_pre_jail_init(TLS_ROLE role)
     	maps_create(VAR_TLS_SERVER_SNI_MAPS, var_tls_server_sni_maps, flags);
     }
     
    +int tls_cert_cb(SSL *ssl, void *arg)
    +{
    +    TLS_SESS_STATE *TLScontext = arg;
    +    const char *cp = TLScontext->peer_sni;
    +    const char *pem;
    +
    +    if (!cp || !tls_server_sni_maps)
    +	return 1;
    +
    +    do {
    +	/* Don't silently skip maps opened with the wrong flags. */
    +	pem = maps_file_find(tls_server_sni_maps, cp, 0);
    +    } while (!pem
    +	     && !tls_server_sni_maps->error
    +	     && (cp = strchr(cp + 1, '.')) != 0);
    +
    +    if (!pem) {
    +	if (tls_server_sni_maps->error) {
    +	    msg_warn("%s: %s map lookup problem",
    +		     tls_server_sni_maps->title, TLScontext->peer_sni);
    +	    return 0;
    +	}
    +	msg_info("TLS SNI %s from %s not matched, using default chain",
    +		 TLScontext->peer_sni, TLScontext->namaddr);
    +        return 1;
    +    }
    +
    +    SSL_certs_clear(ssl);
    +    if (tls_load_pem_chain(ssl, pem, TLScontext->peer_sni) == 0)
    +        return 1;
    +
    +    /* errors already logged */
    +    return 0;
    +}
    +
     /* server_sni_callback - process client's SNI extension */
     
    -static int server_sni_callback(SSL *ssl, int *alert, void *arg)
    +static int server_sni_callback(SSL *ssl, int *alert, void *unused)
     {
    -    SSL_CTX *sni_ctx = (SSL_CTX *) arg;
         TLS_SESS_STATE *TLScontext = SSL_get_ex_data(ssl, TLScontext_index);
         const char *sni = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
    -    const char *cp = sni;
    -    const char *pem;
     
         /* SNI is silently ignored when we don't care or is NULL or empty */
    -    if (!sni_ctx || !tls_server_sni_maps || !sni || !*sni)
    +    if (!tls_server_sni_maps || !sni || !*sni)
     	return SSL_TLSEXT_ERR_NOACK;
     
         if (!valid_hostname(sni, DONT_GRIPE)) {
    @@ -885,37 +917,7 @@ static int server_sni_callback(SSL *ssl, int *alert, void *arg)
     		 TLScontext->namaddr, TLScontext->peer_sni, sni);
     	return SSL_TLSEXT_ERR_NOACK;
         }
    -    do {
    -	/* Don't silently skip maps opened with the wrong flags. */
    -	pem = maps_file_find(tls_server_sni_maps, cp, 0);
    -    } while (!pem
    -	     && !tls_server_sni_maps->error
    -	     && (cp = strchr(cp + 1, '.')) != 0);
    -
    -    if (!pem) {
    -	if (tls_server_sni_maps->error) {
    -	    msg_warn("%s: %s map lookup problem",
    -		     tls_server_sni_maps->title, sni);
    -	    *alert = SSL_AD_INTERNAL_ERROR;
    -	    return SSL_TLSEXT_ERR_ALERT_FATAL;
    -	}
    -	msg_info("TLS SNI %s from %s not matched, using default chain",
    -		 sni, TLScontext->namaddr);
     
    -	/*
    -	 * XXX: We could lie and pretend to accept the name, but since we've
    -	 * previously not implemented the callback (with OpenSSL then
    -	 * declining the extension), and nothing bad happened, declining it
    -	 * explicitly should be safe.
    -	 */
    -	return SSL_TLSEXT_ERR_NOACK;
    -    }
    -    SSL_set_SSL_CTX(ssl, sni_ctx);
    -    if (tls_load_pem_chain(ssl, pem, sni) != 0) {
    -	/* errors already logged */
    -	*alert = SSL_AD_INTERNAL_ERROR;
    -	return SSL_TLSEXT_ERR_ALERT_FATAL;
    -    }
         TLScontext->peer_sni = mystrdup(sni);
         return SSL_TLSEXT_ERR_OK;
     }
    @@ -1296,8 +1298,7 @@ void    tls_log_summary(TLS_ROLE role, TLS_USAGE usage, TLS_SESS_STATE *ctx)
     
     /* tls_alloc_app_context - allocate TLS application context */
     
    -TLS_APPL_STATE *tls_alloc_app_context(SSL_CTX *ssl_ctx, SSL_CTX *sni_ctx,
    -				              int log_mask)
    +TLS_APPL_STATE *tls_alloc_app_context(SSL_CTX *ssl_ctx, int log_mask)
     {
         TLS_APPL_STATE *app_ctx;
     
    @@ -1306,16 +1307,13 @@ TLS_APPL_STATE *tls_alloc_app_context(SSL_CTX *ssl_ctx, SSL_CTX *sni_ctx,
         /* See portability note below with other memset() call. */
         memset((void *) app_ctx, 0, sizeof(*app_ctx));
         app_ctx->ssl_ctx = ssl_ctx;
    -    app_ctx->sni_ctx = sni_ctx;
         app_ctx->log_mask = log_mask;
     
         /* See also: cache purging code in tls_set_ciphers(). */
         app_ctx->cache_type = 0;
     
    -    if (tls_server_sni_maps) {
    +    if (tls_server_sni_maps)
     	SSL_CTX_set_tlsext_servername_callback(ssl_ctx, server_sni_callback);
    -	SSL_CTX_set_tlsext_servername_arg(ssl_ctx, (void *) sni_ctx);
    -    }
         return (app_ctx);
     }
     
    @@ -1325,8 +1323,6 @@ void    tls_free_app_context(TLS_APPL_STATE *app_ctx)
     {
         if (app_ctx->ssl_ctx)
     	SSL_CTX_free(app_ctx->ssl_ctx);
    -    if (app_ctx->sni_ctx)
    -	SSL_CTX_free(app_ctx->sni_ctx);
         if (app_ctx->cache_type)
     	myfree(app_ctx->cache_type);
         myfree((void *) app_ctx);
    diff --git a/postfix/src/tls/tls_server.c b/postfix/src/tls/tls_server.c
    index 7dd3a0169..d38b3b331 100644
    --- a/postfix/src/tls/tls_server.c
    +++ b/postfix/src/tls/tls_server.c
    @@ -425,7 +425,6 @@ static int trust_server_ccerts(X509_STORE_CTX *ctx, void *unused)
     TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     {
         SSL_CTX *server_ctx;
    -    SSL_CTX *sni_ctx;
         X509_STORE *cert_store;
         long    off = 0;
         int     verify_flags = SSL_VERIFY_NONE;
    @@ -524,24 +523,15 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     	tls_print_errors();
     	return (0);
         }
    -    sni_ctx = SSL_CTX_new(TLS_server_method());
    -    if (sni_ctx == 0) {
    -	SSL_CTX_free(server_ctx);
    -	msg_warn("cannot allocate server SNI SSL_CTX: disabling TLS support");
    -	tls_print_errors();
    -	return (0);
    -    }
     #ifdef SSL_SECOP_PEER
         /* Backwards compatible security as a base for opportunistic TLS. */
         SSL_CTX_set_security_level(server_ctx, 0);
    -    SSL_CTX_set_security_level(sni_ctx, 0);
     #endif
     
         /*
          * See the verify callback in tls_verify.c
          */
         SSL_CTX_set_verify_depth(server_ctx, props->verifydepth + 1);
    -    SSL_CTX_set_verify_depth(sni_ctx, props->verifydepth + 1);
     
         /*
          * The session cache is implemented by the tlsmgr(8) server.
    @@ -630,8 +620,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     	SSL_CTX_set_options(server_ctx, TLS_SSL_OP_PROTOMASK(protomask));
         SSL_CTX_set_min_proto_version(server_ctx, min_proto);
         SSL_CTX_set_max_proto_version(server_ctx, max_proto);
    -    SSL_CTX_set_min_proto_version(sni_ctx, min_proto);
    -    SSL_CTX_set_max_proto_version(sni_ctx, max_proto);
     
         /*
          * Some sites may want to give the client less rope. On the other hand,
    @@ -642,17 +630,11 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
         if (var_tls_preempt_clist)
     	SSL_CTX_set_options(server_ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
     
    -    /* Done with server_ctx options, clone to sni_ctx */
    -    SSL_CTX_clear_options(sni_ctx, ~0);
    -    SSL_CTX_set_options(sni_ctx, SSL_CTX_get_options(server_ctx));
    -
         /*
          * Set the call-back routine to debug handshake progress.
          */
    -    if (log_mask & TLS_LOG_DEBUG) {
    +    if (log_mask & TLS_LOG_DEBUG)
     	SSL_CTX_set_info_callback(server_ctx, tls_info_callback);
    -	SSL_CTX_set_info_callback(sni_ctx, tls_info_callback);
    -    }
     
         /*
          * Load the CA public key certificates for both the server cert and for
    @@ -669,7 +651,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     				    props->CAfile, props->CApath) < 0) {
     	/* tls_set_ca_certificate_info() already logs a warning. */
     	SSL_CTX_free(server_ctx);		/* 200411 */
    -	SSL_CTX_free(sni_ctx);
     	return (0);
         }
     
    @@ -678,15 +659,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
          * for the client, they're good enough for us.
          */
         tls_enable_server_rpk(server_ctx, NULL);
    -    tls_enable_server_rpk(sni_ctx, NULL);
    -
    -    /*
    -     * Upref and share the cert store.  Sadly we can't yet use
    -     * SSL_CTX_set1_cert_store(3) which was added in OpenSSL 1.1.0.
    -     */
    -    cert_store = SSL_CTX_get_cert_store(server_ctx);
    -    X509_STORE_up_ref(cert_store);
    -    SSL_CTX_set_cert_store(sni_ctx, cert_store);
     
         /*
          * Load the server public key certificate and private key from file and
    @@ -710,7 +682,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     					props->eckey_file) < 0) {
     	/* tls_set_my_certificate_key_info() already logs a warning. */
     	SSL_CTX_free(server_ctx);		/* 200411 */
    -	SSL_CTX_free(sni_ctx);
     	return (0);
         }
     
    @@ -725,7 +696,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
         if (*props->dh1024_param_file != 0)
     	tls_set_dh_from_file(props->dh1024_param_file);
         tls_tmp_dh(server_ctx, 1);
    -    tls_tmp_dh(sni_ctx, 1);
     
         /*
          * Enable EECDH if available, errors are not fatal, we just keep going
    @@ -734,7 +704,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
          * unified "groups" list.
          */
         tls_auto_groups(server_ctx, var_tls_eecdh_auto, var_tls_ffdhe_auto);
    -    tls_auto_groups(sni_ctx, var_tls_eecdh_auto, var_tls_ffdhe_auto);
     
         /*
          * If we want to check client certificates, we have to indicate it in
    @@ -760,8 +729,6 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     	verify_flags = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;
         SSL_CTX_set_verify(server_ctx, verify_flags,
     		       tls_verify_certificate_callback);
    -    SSL_CTX_set_verify(sni_ctx, verify_flags,
    -		       tls_verify_certificate_callback);
         if (props->ask_ccert && *props->CAfile) {
     	STACK_OF(X509_NAME) *calist = SSL_load_client_CA_file(props->CAfile);
     
    @@ -772,29 +739,16 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props)
     	    tls_print_errors();
     	}
     	SSL_CTX_set_client_CA_list(server_ctx, calist);
    -
    -	if (calist != 0 && sk_X509_NAME_num(calist) > 0) {
    -	    calist = SSL_dup_CA_list(calist);
    -
    -	    if (calist == 0) {
    -		msg_warn("error duplicating client CA names for SNI");
    -		tls_print_errors();
    -	    } else {
    -		SSL_CTX_set_client_CA_list(sni_ctx, calist);
    -	    }
    -	}
         }
     
    -    if (props->ask_ccert && var_tls_srvr_ccerts) {
    +    if (props->ask_ccert && var_tls_srvr_ccerts)
     	SSL_CTX_set_cert_verify_callback(server_ctx, trust_server_ccerts, NULL);
    -	SSL_CTX_set_cert_verify_callback(sni_ctx, trust_server_ccerts, NULL);
    -    }
     
         /*
          * Initialize our own TLS server handle, before diving into the details
          * of TLS session cache management.
          */
    -    app_ctx = tls_alloc_app_context(server_ctx, sni_ctx, log_mask);
    +    app_ctx = tls_alloc_app_context(server_ctx, log_mask);
     
         if (cachable || ticketable || props->set_sessid) {
     
    @@ -915,6 +869,9 @@ TLS_SESS_STATE *tls_server_start(const TLS_SERVER_START_PROPS *props)
     	return (0);
         }
     
    +    /* Configure the SNI-based certificate selection callback */
    +    SSL_set_cert_cb(TLScontext->con, tls_cert_cb, TLScontext);
    +
         /*
          * When encryption is mandatory use the 80-bit plus OpenSSL security
          * level.
    -- 
    2.47.3