From cc9df6e80b68d94a53866fb0f213b5e2811ea874 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 21 Aug 2024 20:43:14 -0500 Subject: [PATCH 01/14] Windows build --- .github/workflows/windows.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/windows.yml diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..62a0d2b --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,38 @@ +name: Windows build + +on: [push, pull_request] + +jobs: + build: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-libidn2 + mingw-w64-ucrt-x86_64-libressl + pkgconf + automake + autoconf + make + texinfo + - name: Build + run: | + autoreconf -i + ./configure --with-tls=openssl --with-libidn --prefix=/ libssl_CFLAGS=-I/ucrt64/include/libressl "libssl_LIBS=-llibressl -L@prefix/lib -llibrecrypto" "CFLAGS=-Wno-incompatible-pointer-types -DNOCRYPT" + make + make install-strip DESTDIR=/d/a/msmtp/tmp + cp `ldd src/msmtp | awk '/\/ucrt64/ {print $3}' | uniq` /d/a/msmtp/tmp/bin/ + - name: Artifacts + uses: actions/upload-artifact@v4 + with: + name: msmtp + path: D:\a\msmtp\tmp\ From 64c4b73375254af70ba37df4a6c99e0652223cc5 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Thu, 22 Aug 2024 15:43:33 -0500 Subject: [PATCH 02/14] GitHub release upload --- .github/workflows/windows.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 62a0d2b..cf85fc8 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -31,6 +31,15 @@ jobs: make make install-strip DESTDIR=/d/a/msmtp/tmp cp `ldd src/msmtp | awk '/\/ucrt64/ {print $3}' | uniq` /d/a/msmtp/tmp/bin/ + - name: Upload release to GitHub + if: github.ref_type == 'tag' + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + 7z a msmtp.zip D:\a\msmtp\tmp\* + gh release create ${{ github.ref_name }} --generate-notes || echo "Release already exists!" + gh release upload ${{ github.ref_name }} msmtp.zip - name: Artifacts uses: actions/upload-artifact@v4 with: From 024e96cef981d9186fe9dc57d77277f0610a03b7 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Fri, 23 Aug 2024 23:55:14 -0500 Subject: [PATCH 03/14] Use explicit pointer cast for get/setsockopt Otherwise we need -Wno-incompatible-pointer-types at least with GCC 14.2 on MSYS2/UCRT64 --- src/net.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/net.c b/src/net.c index c4d7171..56a5f58 100644 --- a/src/net.c +++ b/src/net.c @@ -445,7 +445,7 @@ int net_connect(int fd, const struct sockaddr *serv_addr, socklen_t addrlen, /* test for success, set errno */ optlen = sizeof(err); - if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &optlen) < 0) + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)&err, &optlen) < 0) { return -1; } @@ -490,8 +490,8 @@ void net_set_io_timeout(int socket, int seconds) if (seconds > 0) { milliseconds = seconds * 1000; - (void)setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &milliseconds, sizeof(int)); - (void)setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &milliseconds, sizeof(int)); + (void)setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (void*)&milliseconds, sizeof(int)); + (void)setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, (void*)&milliseconds, sizeof(int)); } #else /* UNIX */ struct timeval tv; From fd4a685262f0c93fa3f6a48015c9d7897e729c08 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 11 Sep 2024 13:17:21 -0500 Subject: [PATCH 04/14] Windows native TLS using Schannel SSP --- configure.ac | 19 +- src/Makefile.am | 3 + src/msmtp.c | 2 +- src/mtls-sspi.c | 704 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 src/mtls-sspi.c diff --git a/configure.ac b/configure.ac index e76ebf9..90b3dbe 100644 --- a/configure.ac +++ b/configure.ac @@ -94,11 +94,12 @@ want_tls="yes" want_gnutls="yes" want_openssl="no" want_libtls="no" +want_sspi="no" tls_CFLAGS="" tls_LIBS="" with_ssl_was_used=no -AC_ARG_WITH([tls], [AS_HELP_STRING([--with-tls=[gnutls|openssl|libtls|no]], - [TLS support: GnuTLS (default), OpenSSL (discouraged), libtls, or none.])], +AC_ARG_WITH([tls], [AS_HELP_STRING([--with-tls=[gnutls|openssl|libtls|sspi|no]], + [TLS support: GnuTLS (default), OpenSSL (discouraged), libtls, SSPI (Schannel), or none.])], if test "$withval" = "gnutls"; then want_tls=yes want_gnutls=yes @@ -114,13 +115,17 @@ AC_ARG_WITH([tls], [AS_HELP_STRING([--with-tls=[gnutls|openssl|libtls|no]], want_gnutls=no want_openssl=no want_libtls=yes + elif test "$withval" = "sspi"; then + want_tls=yes + want_gnutls=no + want_sspi=yes elif test "$withval" = "no"; then want_tls=no want_gnutls=no want_openssl=no want_libtls=no else - AC_MSG_ERROR([Use --with-tls=gnutls or --with-tls=openssl or --with-tls=libtls or --with-tls=no]) + [AC_MSG_ERROR([Invalid --with-tls=$withval. Use --with-tls=gnutls|openssl|libtls|sspi|no])] fi) if test "$want_gnutls" = "yes"; then PKG_CHECK_MODULES([libgnutls], [gnutls >= 3.7.2], [HAVE_LIBGNUTLS=1], [HAVE_LIBGNUTLS=0]) @@ -165,15 +170,23 @@ if test "$want_libtls" = "yes" -a "$have_tls" = "no"; then AC_DEFINE([HAVE_LIBTLS], [1], [Define to 1 if libtls is available]) fi fi +if test "$want_sspi" = "yes"; then + have_tls="yes" + tls_lib="Schannel" + tls_LIBS="-lsecur32 -lcrypt32" + AC_DEFINE([HAVE_LIBSSL], [1], [Define to 1 if libssl is available]) +fi if test "$have_tls" = "yes"; then AC_DEFINE([HAVE_TLS], [1], [Define to 1 to build with TLS/SSL support]) elif test "$want_tls" = "yes"; then AC_MSG_WARN([Disabling TLS support, which is very bad. Consider using GnuTLS!]) fi +AC_DEFINE_UNQUOTED([TLS_LIB], ["$tls_lib"], [TLS library used]) AM_CONDITIONAL([HAVE_TLS], [test "$have_tls" = "yes"]) AM_CONDITIONAL([HAVE_GNUTLS], [test "$tls_lib" = "GnuTLS"]) AM_CONDITIONAL([HAVE_OPENSSL], [test "$tls_lib" = "OpenSSL"]) AM_CONDITIONAL([HAVE_LIBTLS], [test "$tls_lib" = "libtls"]) +AM_CONDITIONAL([HAVE_SSPI], [test "$tls_lib" = "Schannel"]) AC_SUBST([tls_CFLAGS]) AC_SUBST([tls_LIBS]) diff --git a/src/Makefile.am b/src/Makefile.am index fcd18e3..44db630 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -32,6 +32,9 @@ endif if HAVE_LIBTLS msmtp_SOURCES += mtls-libtls.c endif +if HAVE_SSPI +msmtp_SOURCES += mtls-sspi.c +endif AM_CPPFLAGS = $(tls_CFLAGS) $(libgsasl_CFLAGS) $(libidn2_CFLAGS) $(libsecret_CFLAGS) diff --git a/src/msmtp.c b/src/msmtp.c index 95297a6..1d708a4 100644 --- a/src/msmtp.c +++ b/src/msmtp.c @@ -2177,7 +2177,7 @@ void msmtp_print_version(void) #ifdef HAVE_LIBGNUTLS "GnuTLS" #elif defined (HAVE_LIBSSL) - "OpenSSL" + TLS_LIB #elif defined (HAVE_LIBTLS) "libtls" #else diff --git a/src/mtls-sspi.c b/src/mtls-sspi.c new file mode 100644 index 0000000..4b0e3a5 --- /dev/null +++ b/src/mtls-sspi.c @@ -0,0 +1,704 @@ +/* + * mtls-sspi.c Schannel SSP implementation for TLS using SSPI (W32 ONLY) + * + * This file is part of msmtp, an SMTP client, and of mpop, a POP3 client. + * + * Copyright (C) 2024 Mikhail Titov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#pragma warning(error:4013) + +#include +#include +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#define SECURITY_WIN32 +#include +#define SCHANNEL_USE_BLACKLISTS +#include +#include +#include + +#pragma comment (lib, "secur32.lib") +#pragma comment(lib, "crypt32.Lib") + +#include "gettext.h" +#define _(string) gettext(string) +#define N_(string) gettext_noop(string) + +#include "xalloc.h" +#include "readbuf.h" +#include "tools.h" +#include "mtls.h" + +/* + * epoch is Jan. 1, 1601: 134774 days to Jan. 1, 1970 + * https://learn.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time + * https://devblogs.microsoft.com/oldnewthing/20220602-00/?p=106706 + */ +#define filetime_to_timet(ft) (*(ULONGLONG*)&ft / 10000000ULL - 11644473600ULL) +#define MAKE_DESC(buf) { SECBUFFER_VERSION, ARRAYSIZE(buf), buf }; +#define TLS_MAX_PACKET_SIZE (16384+512) // payload + extra over head for header/mac/padding (probably an overestimate) + +/* very handy private functions from net.c */ +int net_send(int fd, const void* buf, size_t len, char** errstr); +int net_recv(int fd, void* buf, size_t len, char** errstr); + +struct mtls_internals_t +{ + int fd; + CredHandle handle; + CtxtHandle context; + SecPkgContext_StreamSizes sizes; + char incoming[TLS_MAX_PACKET_SIZE]; + char* incoming_ptr; // where we left off decrypting (SECBUFFER_EXTRA only actually) +}; + +/* + * mtls_lib_init() + * + * see mtls.h + */ + +int mtls_lib_init(char **errstr) +{ + return TLS_EOK; +} + +/* + * mtls_cert_info_get() + * + * see mtls.h + */ + +int mtls_cert_info_get(mtls_t *mtls, mtls_cert_info_t *mtci, char **errstr) +{ + const char *errmsg = _("cannot get TLS certificate info"); + CHAR name[200]; + DWORD hashsize; + PCCERT_CONTEXT cert_context; + + QueryContextAttributes(&mtls->internals->context, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &cert_context); + if (CertNameToStr(cert_context->dwCertEncodingType, &cert_context->pCertInfo->Subject, + CERT_X500_NAME_STR, name, sizeof(name))) + { + mtci->subject_info = xstrdup(name); + } + + if (CertNameToStr(cert_context->dwCertEncodingType, &cert_context->pCertInfo->Issuer, + CERT_X500_NAME_STR, name, sizeof(name))) + { + mtci->issuer_info = xstrdup(name); + } + + mtci->activation_time = filetime_to_timet(cert_context->pCertInfo->NotBefore); + mtci->expiration_time = filetime_to_timet(cert_context->pCertInfo->NotAfter); + + hashsize = ARRAYSIZE(mtci->sha256_fingerprint); + if (!CryptHashCertificate2(BCRYPT_SHA256_ALGORITHM, 0, NULL, cert_context->pbCertEncoded, + cert_context->cbCertEncoded, mtci->sha256_fingerprint, &hashsize)) + { + *errstr = xasprintf(_("%s: error getting SHA256 fingerprint"), errmsg); + return TLS_ECERT; + } + /* deprecated + hashsize = ARRAYSIZE(mtci->sha1_fingerprint); + if (!CryptHashCertificate2(BCRYPT_SHA1_ALGORITHM, 0, NULL, cert_context->pbCertEncoded, + cert_context->cbCertEncoded, mtci->sha1_fingerprint, &hashsize)) + { + *errstr = xasprintf(_("%s: error getting SHA1 fingerprint"), errmsg); + return TLS_ECERT; + } + */ + + return TLS_EOK; +} + +/* + * mtls_check_cert() + * + * Schannel will perform a full certificate verification automatically + * if SCH_CRED_AUTO_CRED_VALIDATION and SCH_CRED_REVOCATION_CHECK_CHAIN are set + * and SCH_CRED_NO_SERVERNAME_CHECK is cleared. + * + * If 'mtls->have_sha256_fingerprint' flags is set, compare the + * 'mtls->fingerprint' data with the peer certificate's fingerprint. If this + * succeeds, the connection can be considered secure. + * + * Used error codes: TLS_ECERT + */ + +static int mtls_check_cert(mtls_t *mtls, char **errstr) +{ + const char* error_msg = _("TLS certificate verification failed"); + + if (mtls->have_sha256_fingerprint) + { + unsigned char sha256_fingerprint[32]; + DWORD cbComputedHash = ARRAYSIZE(sha256_fingerprint); + PCCERT_CONTEXT pRemoteCertContext; + + if (SEC_E_OK != QueryContextAttributes(&mtls->internals->context, + SECPKG_ATTR_REMOTE_CERT_CONTEXT, &pRemoteCertContext)) + { + *errstr = xasprintf(_("%s: no certificate was sent"), error_msg); + return TLS_ECERT; + } + + if (!CryptHashCertificate2(BCRYPT_SHA256_ALGORITHM, 0, NULL, pRemoteCertContext->pbCertEncoded, + pRemoteCertContext->cbCertEncoded, sha256_fingerprint, &cbComputedHash)) + { + *errstr = xasprintf(_("%s: error getting SHA256 fingerprint"), error_msg); + return TLS_ECERT; + } + + if (memcmp(sha256_fingerprint, mtls->fingerprint, 32) != 0) + { + *errstr = xasprintf(_("%s: the certificate fingerprint does not match"), error_msg); + return TLS_ECERT; + } + } + + return TLS_EOK; +} + + +/* + * mtls_init() + * + * see mtls.h + */ + +int mtls_init(mtls_t *mtls, + const char *key_file, const char *cert_file, const char *pin, + const char *trust_file, const char *crl_file, + const unsigned char *sha256_fingerprint, + const unsigned char *sha1_fingerprint, + const unsigned char *md5_fingerprint, + int min_dh_prime_bits, const char *priorities, + const char *hostname, + int no_certcheck, + char **errstr) +{ + char* prio_copy; + TLS_PARAMETERS tls_params = { 0 }; + SCH_CREDENTIALS cred = + { + .dwVersion = SCH_CREDENTIALS_VERSION, + .dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS + | SCH_CRED_REVOCATION_CHECK_CHAIN * (!no_certcheck) + | SCH_CRED_AUTO_CRED_VALIDATION * (!no_certcheck) + | SCH_CRED_NO_SERVERNAME_CHECK * no_certcheck, + .cTlsParameters = 1, + .pTlsParameters = &tls_params + }; + + if (sha1_fingerprint || md5_fingerprint) + { + *errstr = xasprintf( + _("cannot use deprecated fingerprints, please update to SHA256")); + return TLS_ELIBFAILED; + } + if (min_dh_prime_bits >= 0) + { + /* This will never need to be implemented because it is deprecated. + * But we should report it and not just silently ignore it. */ + *errstr = xasprintf( + _("cannot set minimum number of DH prime bits for TLS: %s"), + _("feature not yet implemented for Schannel SSP")); + return TLS_ELIBFAILED; + } + /* + * Basic support for protocol restrictions. + * We mimic libtls string but will unofficially accept minor deviations from that format. + */ + if (priorities) + { + size_t len = strlen(priorities); + prio_copy = xmalloc(len+1); /* for modification by strtok() */ + const char* key; + char* value; + char* token = NULL; + DWORD enabled = 0; + int failed = 0; + for (int i = 0; i <= len; ++i) + prio_copy[i] = __isascii(priorities[i]) && isupper(priorities[i]) ? _tolower(priorities[i]) : priorities[i]; + if ((key = strstr(prio_copy, "protocols=")) != NULL) + { + const DWORD tls = 't' | 'l' << 8 | 's' << 16; // little-endian only + value = prio_copy + (key + strlen("protocols=") - prio_copy); + (void)strtok(value, " "); + token = strtok(value, ","); + while (token) + { + if ((*(DWORD*)token & 0x00fffffful) != tls) + { + *errstr = xasprintf( + _("error in priority string at position %d"), + token - prio_copy + 1); + goto error_prio2; + } + token += 3; + if (*token == 'v') + token++; + if (*token++ != '1') + goto error_prio; + if (*token == '.' || *token == '_') + token++; + switch (*token) + { + case '1': enabled |= SP_PROT_TLS1_1_CLIENT; break; + case '2': enabled |= SP_PROT_TLS1_2_CLIENT; break; + case '3': enabled |= SP_PROT_TLS1_3_CLIENT; break; + default: goto error_prio; + } + + token = strtok(NULL, ","); + } + free(prio_copy); + tls_params.grbitDisabledProtocols = ~enabled; + } + } + /* FIXME: Implement support for 'crl_file' */ + if (trust_file && crl_file) + { + *errstr = xasprintf( + _("cannot load CRL file: %s"), + _("feature not yet implemented for Schannel SSP")); + return TLS_ELIBFAILED; + } + + if (sha256_fingerprint && !no_certcheck) + { + memcpy(mtls->fingerprint, sha256_fingerprint, 32); + mtls->have_sha256_fingerprint = 1; + } + + mtls->internals = xmalloc(sizeof(struct mtls_internals_t)); + memset(mtls->internals, 0, sizeof(struct mtls_internals_t)); + + SECURITY_STATUS status = AcquireCredentialsHandle(NULL, UNISP_NAME, SECPKG_CRED_OUTBOUND, NULL, + &cred, NULL, NULL, &mtls->internals->handle, NULL); + if (SEC_E_OK == status) + { + mtls->hostname = xstrdup(hostname); + mtls->no_certcheck = no_certcheck; + return TLS_EOK; + } + + char buf[11]; + sprintf(buf, "0x%0x", status); + *errstr = xasprintf(_("cannot initialize TLS library: %s"), buf); + free(mtls->internals); + mtls->internals = NULL; + return TLS_ELIBFAILED; + +error_prio: + *errstr = xasprintf( + _("cannot set priorities for TLS session: %s"), + _("protocol not supported")); +error_prio2: + free(prio_copy); + return TLS_ELIBFAILED; +} + +/* + * mtls_start() + * + * see mtls.h + */ + +int mtls_start(mtls_t* mtls, int fd, + mtls_cert_info_t* mtci, char** mtls_parameter_description, char** errstr) +{ + int error_code = TLS_EHANDSHAKE; + struct mtls_internals_t* const s = mtls->internals; + s->fd = fd; + PCtxtHandle context = NULL; + int received = 0; + s->incoming_ptr = s->incoming; + for (;;) + { + SecBuffer inbuffers[2] = { 0 }; + inbuffers[0].BufferType = SECBUFFER_TOKEN; + inbuffers[0].pvBuffer = s->incoming_ptr; + inbuffers[0].cbBuffer = received; + inbuffers[1].BufferType = SECBUFFER_EMPTY; + + SecBuffer outbuffers[3] = { 0 }; + outbuffers[0].BufferType = SECBUFFER_TOKEN; + outbuffers[1].BufferType = SECBUFFER_ALERT; + outbuffers[2].BufferType = SECBUFFER_EMPTY; + + SecBufferDesc indesc = MAKE_DESC(inbuffers); + SecBufferDesc outdesc = MAKE_DESC(outbuffers); + + DWORD flags = ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY + | ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_STREAM; + SECURITY_STATUS status = InitializeSecurityContext( + &s->handle, + context, + context ? NULL : mtls->hostname, + flags, + 0, + 0, + context ? &indesc : NULL, + 0, + context ? NULL : &s->context, + &outdesc, + &flags, + NULL); + context = &s->context; + + if (inbuffers[1].BufferType == SECBUFFER_EXTRA) + { + s->incoming_ptr += received - inbuffers[1].cbBuffer; + received = inbuffers[1].cbBuffer; + } + else if (inbuffers[1].BufferType != SECBUFFER_MISSING) + { + received = 0; + s->incoming_ptr = s->incoming; + } + + if (status == SEC_E_OK) + { + if (outbuffers[0].BufferType != SECBUFFER_TOKEN || outbuffers[0].cbBuffer == 0) + break; + + /* TLS1.3 send token back to server */ + if (net_send(fd, outbuffers[0].pvBuffer, outbuffers[0].cbBuffer, errstr) < 0) + { + return TLS_EHANDSHAKE; + } + break; + } + else if (status == SEC_I_CONTINUE_NEEDED) + { + char* buffer = outbuffers[0].pvBuffer; + int size = outbuffers[0].cbBuffer; + + while (size) + { + int d = net_send(fd, buffer, size, errstr); + if (d <= 0) + break; + + size -= d; + buffer += d; + } + if (outbuffers[0].pvBuffer) + status = FreeContextBuffer(outbuffers[0].pvBuffer); + if (size != 0) + goto error; + } + else if (status == SEC_E_WRONG_PRINCIPAL) + { + *errstr = xasprintf(_("%s: the certificate owner does not match hostname %s"), + _("TLS certificate verification failed"), mtls->hostname); + goto error; + } + else if (status != SEC_E_INCOMPLETE_MESSAGE) + { + char* buf; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, status, GetUserDefaultLangID(), (LPSTR)&buf, 0, NULL); + int len = strlen(buf); + buf[len - 2] = 0; /* remove \r\n */ + *errstr = xasprintf(_("cannot initialize TLS session: %s"), buf); + LocalFree(buf); + goto error; + } + + if (received == sizeof(s->incoming)) + { + *errstr = xasprintf(_("cannot initialize TLS session: %s"), _("no buffer space available")); + goto error; + } + int r = net_recv(fd, s->incoming_ptr + received, sizeof(s->incoming) - received - (s->incoming_ptr - s->incoming), errstr); + + if (r <= 0) + goto error; + + received += r; + } + + if (mtci && ((error_code = mtls_cert_info_get(mtls, mtci, errstr)) != TLS_EOK)) + goto error; + if (mtls_parameter_description) + { + size_t converted; + char suite[SZ_ALG_MAX_SIZE]; + char proto[11]; + static const int desc_size = 200; + SecPkgContext_CipherInfo cipher_info; + SecPkgContext_ConnectionInfo conn_info; + + *mtls_parameter_description = xmalloc(desc_size); + QueryContextAttributes(context, SECPKG_ATTR_CIPHER_INFO, &cipher_info); + wcstombs_s(&converted, suite, SZ_ALG_MAX_SIZE, cipher_info.szCipherSuite, _TRUNCATE); + QueryContextAttributes(context, SECPKG_ATTR_CONNECTION_INFO, &conn_info); + switch (conn_info.dwProtocol) + { + case SP_PROT_TLS1_1_CLIENT: + strcpy(proto, "TLS1.1"); + break; + case SP_PROT_TLS1_2_CLIENT: + strcpy(proto, "TLS1.2"); + break; + case SP_PROT_TLS1_3_CLIENT: + mtls->is_tls_1_3_or_newer = 1; + strcpy(proto, "TLS1.3"); + break; + default: + sprintf(proto, "0x%x ", conn_info.dwProtocol); + } + sprintf(*mtls_parameter_description, + "%s \x1B]8;;https://ciphersuite.info/cs/%s/\x1b\\%s\x1b]8;;\x1b\\", + proto, suite, suite); + } + if (!mtls->no_certcheck && ((error_code = mtls_check_cert(mtls, errstr)) != TLS_EOK)) + goto error; + + QueryContextAttributes(context, SECPKG_ATTR_STREAM_SIZES, &s->sizes); + mtls->is_active = 1; + return TLS_EOK; + +error: + DeleteSecurityContext(context); + FreeCredentialsHandle(&s->handle); + return error_code; +} + + +/* + * mtls_readbuf_read() + * + * Wraps TLS read function to provide buffering for mtls_gets(). + */ + +int mtls_readbuf_read(mtls_t *mtls, readbuf_t *readbuf, char *ptr, + char **errstr) +{ + struct mtls_internals_t* const s = mtls->internals; + + s->incoming_ptr = s->incoming; + int received = 0; + while (readbuf->count <= 0) + { + if (received != 0) + { + SecBuffer buffers[4]; + assert(s->sizes.cBuffers == ARRAYSIZE(buffers)); + + buffers[0].BufferType = SECBUFFER_DATA; + buffers[0].pvBuffer = s->incoming_ptr; + buffers[0].cbBuffer = received; + buffers[1].BufferType = SECBUFFER_EMPTY; + buffers[2].BufferType = SECBUFFER_EMPTY; + buffers[3].BufferType = SECBUFFER_EMPTY; + + SecBufferDesc desc = MAKE_DESC(buffers); + + SECURITY_STATUS sec = DecryptMessage(&s->context, &desc, 0, NULL); + if (sec == SEC_E_OK) + { + assert(buffers[0].BufferType == SECBUFFER_STREAM_HEADER); + assert(buffers[1].BufferType == SECBUFFER_DATA); + assert(buffers[2].BufferType == SECBUFFER_STREAM_TRAILER); + + readbuf->ptr = buffers[1].pvBuffer; + readbuf->count = buffers[1].cbBuffer; + if (buffers[3].BufferType == SECBUFFER_EXTRA) + { + s->incoming_ptr += received - buffers[3].cbBuffer; + received = buffers[3].cbBuffer; + } + else + { + received = 0; + s->incoming_ptr = s->incoming; + } + + break; + } + else if (sec == SEC_I_RENEGOTIATE && mtls->is_tls_1_3_or_newer) + { + /* + * TLS1.3 repurposed status code. + * If TLS<1.3 server wants to renegotiate TLS connection that we don't support. + */ + assert(buffers[3].BufferType == SECBUFFER_EXTRA); + /* new_session_ticket */ + assert(((BYTE*)buffers[3].pvBuffer)[5] == 0x04); + SecBuffer inbuffers[2] = { 0 }; + inbuffers[0].BufferType = SECBUFFER_TOKEN; + inbuffers[0].pvBuffer = buffers[3].pvBuffer; + inbuffers[0].cbBuffer = buffers[3].cbBuffer; + inbuffers[1].BufferType = SECBUFFER_EMPTY; + + SecBuffer outbuffers[3] = { 0 }; + outbuffers[0].BufferType = SECBUFFER_TOKEN; + outbuffers[1].BufferType = SECBUFFER_ALERT; + outbuffers[2].BufferType = SECBUFFER_EMPTY; + + SecBufferDesc indesc = MAKE_DESC(inbuffers); + SecBufferDesc outdesc = MAKE_DESC(outbuffers); + + DWORD flags = ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT + | ISC_REQ_STREAM | ISC_RET_EXTENDED_ERROR; + SECURITY_STATUS sec = InitializeSecurityContext( + &s->handle, + &s->context, + NULL, + flags, + 0, + 0, + &indesc, + 0, + NULL, + &outdesc, + &flags, + NULL); + if (sec != SEC_E_OK || inbuffers[1].BufferType != SECBUFFER_EXTRA) + { + return TLS_EIO; + } + + s->incoming_ptr += received - inbuffers[1].cbBuffer; + received = inbuffers[1].cbBuffer; + assert(received>=0); + continue; + } + else if (sec != SEC_E_INCOMPLETE_MESSAGE) + { + return TLS_EIO; + } + } + + if (received == sizeof(s->incoming)) + { + *errstr = xasprintf(_("network read error: %s"), _("no buffer space available")); + return TLS_EIO; + } + + int r = net_recv(s->fd, s->incoming_ptr + received, sizeof(s->incoming) - received - (s->incoming_ptr - s->incoming), errstr); + if (r <= 0) + return TLS_EIO; + + received += r; + } + + readbuf->count--; + *ptr = *((readbuf->ptr)++); + + return 1; +} + + +/* + * mtls_puts() + * + * see mtls.h + */ +char wbuffer[TLS_MAX_PACKET_SIZE]; + +int mtls_puts(mtls_t *mtls, const char *ss, size_t len, char **errstr) +{ + struct mtls_internals_t* const s = mtls->internals; + assert(s->sizes.cbHeader + s->sizes.cbMaximumMessage + s->sizes.cbTrailer <= sizeof(wbuffer)); + assert(len <= s->sizes.cbMaximumMessage); + + SecBuffer buffers[3]; + buffers[0].BufferType = SECBUFFER_STREAM_HEADER; + buffers[0].pvBuffer = wbuffer; + buffers[0].cbBuffer = s->sizes.cbHeader; + buffers[1].BufferType = SECBUFFER_DATA; + buffers[1].pvBuffer = wbuffer + s->sizes.cbHeader; + buffers[1].cbBuffer = (unsigned long)len; + buffers[2].BufferType = SECBUFFER_STREAM_TRAILER; + buffers[2].pvBuffer = wbuffer + s->sizes.cbHeader + len; + buffers[2].cbBuffer = s->sizes.cbTrailer; + + memcpy(buffers[1].pvBuffer, ss, len); + + SecBufferDesc desc = { SECBUFFER_VERSION, ARRAYSIZE(buffers), buffers }; + SECURITY_STATUS status = EncryptMessage(&s->context, 0, &desc, 0); + if (status != SEC_E_OK) + { + char buf[11]; + sprintf(buf, "0x%0x", status); + *errstr = xasprintf(_("cannot write to TLS connection: %s"), buf); + return TLS_ELIBFAILED; + } + + int total = buffers[0].cbBuffer + buffers[1].cbBuffer + buffers[2].cbBuffer; + int sent = 0; + while (sent != total) + { + int d = net_send(mtls->internals->fd, wbuffer + sent, total - sent, errstr); + if (d <= 0) + return TLS_EIO; + + sent += d; + } + + return TLS_EOK; +} + +/* + * mtls_close() + * + * see mtls.h + */ + +void mtls_close(mtls_t *mtls) +{ + SECURITY_STATUS ss; + if (mtls->is_active) + { + ss = DeleteSecurityContext(&mtls->internals->context); + assert(SEC_E_OK == ss); + ss = FreeCredentialsHandle(&mtls->internals->handle); + assert(SEC_E_OK == ss); + } + free(mtls->internals); + mtls->internals = NULL; + if (mtls->hostname) + { + free(mtls->hostname); + } + mtls_clear(mtls); +} + + +/* + * mtls_lib_deinit() + * + * see mtls.h + */ + +void mtls_lib_deinit(void) +{ +} From 817f74ab277298d00626b9246266abe6c021f380 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Mon, 16 Sep 2024 13:45:00 -0500 Subject: [PATCH 05/14] Windows native DNS query for SRV --- configure.ac | 2 ++ src/msmtp.c | 2 +- src/net.c | 42 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 90b3dbe..f206d95 100644 --- a/configure.ac +++ b/configure.ac @@ -79,6 +79,8 @@ else fi if test "$found_res_query" = "yes"; then AC_DEFINE([HAVE_LIBRESOLV], [1], [Define to 1 if libresolv is available]) +else + case "${target}" in *-*-mingw*) LIBS="$LIBS -ldnsapi" ;; esac fi dnl pkg-config (required to detect libraries) diff --git a/src/msmtp.c b/src/msmtp.c index 1d708a4..24088f5 100644 --- a/src/msmtp.c +++ b/src/msmtp.c @@ -1682,7 +1682,7 @@ void print_error(const char *format, ...) int msmtp_configure(const char *address, const char *conffile) { -#ifdef HAVE_LIBRESOLV +#if defined(HAVE_LIBRESOLV) || defined(W32_NATIVE) int e; diff --git a/src/net.c b/src/net.c index 56a5f58..1e4f31e 100644 --- a/src/net.c +++ b/src/net.c @@ -64,6 +64,9 @@ #ifdef HAVE_LIBRESOLV # include # include +#elif defined(W32_NATIVE) +# include +# pragma comment(lib, "dnsapi.lib") #endif #include "gettext.h" @@ -1090,16 +1093,15 @@ char* net_get_srv_query(const char *domain, const char *service) */ int net_get_srv_record(const char* query, char **hostname, int *port) { + int current_prio = INT_MAX; + int current_weight = -1; + char *current_hostname = NULL; + int current_port = 0; #ifdef HAVE_LIBRESOLV - unsigned char buffer[NS_PACKETSZ]; int response_len; ns_msg msg; int i; - int current_prio = INT_MAX; - int current_weight = -1; - char *current_hostname = NULL; - int current_port = 0; response_len = res_query(query, ns_c_in, ns_t_srv, buffer, sizeof(buffer)); if (response_len < 0) { @@ -1139,6 +1141,36 @@ int net_get_srv_record(const char* query, char **hostname, int *port) return NET_EOK; } +#elif defined(W32_NATIVE) + int error_code = NET_EIO; + PDNS_RECORD record; + DNS_STATUS status = DnsQuery_A(query, DNS_TYPE_SRV, DNS_QUERY_STANDARD, NULL, &record, NULL); + if (status != ERROR_SUCCESS) + return NET_ESRVNOTFOUND; + for (PDNS_RECORD r = record; r; r = r->pNext) + { + if (r->wType != DNS_TYPE_SRV) + continue; + int prio, weight; + prio = r->Data.SRV.wPriority; + weight = r->Data.SRV.wWeight; + if (prio < current_prio || (prio == current_prio && weight > current_weight)) + { + current_hostname = r->Data.SRV.pNameTarget; + current_port = r->Data.SRV.wPort; + current_prio = prio; + current_weight = weight; + } + } + if (current_hostname) + { + *hostname = xstrdup(current_hostname); + *port = current_port; + error_code = NET_EOK; + } + DnsRecordListFree(record, DnsFreeRecordList); + + return error_code; #else return NET_ELIBFAILED; From 8664e6d4086c7f1892813c6a9eb0021c4ad25329 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Thu, 19 Sep 2024 15:15:59 -0500 Subject: [PATCH 06/14] Windows native IDN # Conflicts: # configure.ac --- configure.ac | 4 ++++ src/msmtp.c | 12 +++++++++++- src/net.c | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index f206d95..b7d7b74 100644 --- a/configure.ac +++ b/configure.ac @@ -295,6 +295,10 @@ if test "$libidn" != "no"; then AC_MSG_WARN([$libidn_PKG_ERRORS]) AC_MSG_WARN([libidn is provided by GNU Libidn]) libidn="no" + case "${target}" in *-*-mingw*) + AC_MSG_WARN([Using Windows native IDN support]) + want_libidn=no + esac else libidn="yes" AC_DEFINE([HAVE_LIBIDN], [1], [Define to 1 if libidn is available]) diff --git a/src/msmtp.c b/src/msmtp.c index 24088f5..8884954 100644 --- a/src/msmtp.c +++ b/src/msmtp.c @@ -70,6 +70,7 @@ extern int optind; #ifdef W32_NATIVE #define SYSCONFFILE "msmtprc.txt" #define USERCONFFILE "msmtprc.txt" +#include /* setlocale for native IDN */ #else /* UNIX */ #define SYSCONFFILE "msmtprc" #define USERCONFFILE ".msmtprc" @@ -2222,7 +2223,7 @@ void msmtp_print_version(void) printf("\n"); /* Internationalized Domain Names support */ printf(_("IDN support: ")); -#if defined(HAVE_LIBIDN) \ +#if defined(HAVE_LIBIDN) || defined(W32_NATIVE) && !defined(_UNICODE) \ || (defined(HAVE_GAI_IDN) && (!defined(HAVE_TLS) \ || (defined(HAVE_LIBGNUTLS) && GNUTLS_VERSION_NUMBER >= 0x030400))) printf(_("enabled")); @@ -3762,6 +3763,15 @@ void msmtp_print_conf(msmtp_cmdline_conf_t conf, account_t *account) int main(int argc, char *argv[]) { +#if defined(W32_NATIVE) && !defined(_UNICODE) && !defined(HAVE_LIBIDN)// && !defined(ENABLE_NLS) + /* We need this for proper IDN conversion later on using Windows native way + * AND proper console output. + * > At program startup, the equivalent of the following statement is executed : + * > setlocale(LC_ALL, "C"); + * https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale + */ + setlocale(LC_ALL, ""); +#endif msmtp_cmdline_conf_t conf; /* account information from the configuration file(s) */ list_t *account_list = NULL; diff --git a/src/net.c b/src/net.c index 1e4f31e..04bc8bc 100644 --- a/src/net.c +++ b/src/net.c @@ -59,6 +59,9 @@ #ifdef HAVE_LIBIDN # include +#elif defined(W32_NATIVE) +# include +# pragma comment(lib, "normaliz.lib") #endif #ifdef HAVE_LIBRESOLV @@ -754,6 +757,46 @@ int net_open_socket( # endif #elif defined(HAVE_LIBIDN) idn2_to_ascii_lz(hostname, &idn_hostname, IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL); +#elif defined(W32_NATIVE) && !defined(_UNICODE) + size_t conv; + WCHAR hostname_wide[NI_MAXHOST]; + /* We rely on a proper setlocale earlier that was NOT "C" as set default by MS C runtime. */ +#ifdef ENABLE_NLS + /* FIXME: Something messes up locale (at least on MSYS2/UCRT64) and setlocale(LC_ALL, "") won't fix it. + * The commented code below is the way to get proper console output for hostname. + * Otherwise let's make sure we get proper IDN conversion and that is it. */ + char locale_name[LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)]; + GetSystemDefaultLocaleName((LPWSTR)locale_name, LOCALE_NAME_MAX_LENGTH); + size_t len = wcsnlen_s((LPWSTR)locale_name, LOCALE_NAME_MAX_LENGTH); + for (int i = 1; i <= len; ++i) + locale_name[i] = locale_name[i * 2]; + setlocale(LC_ALL, locale_name); + _locale_t locale = _create_locale(LC_ALL, ""); + errno_t err = _mbstowcs_s_l(&conv, hostname_wide, ARRAYSIZE(hostname_wide), hostname, _TRUNCATE, locale); + _free_locale(locale); +#else + errno_t err = mbstowcs_s(&conv, hostname_wide, ARRAYSIZE(hostname_wide), hostname, _TRUNCATE); +#endif + if (!err) + { + int i; + size_t hostname_length = wcsnlen_s(hostname_wide, ARRAYSIZE(hostname_wide)); + idn_hostname = xmalloc(NI_MAXHOST * sizeof(WCHAR)); + int result = IdnToAscii(0, hostname_wide, hostname_length, (LPWSTR)idn_hostname, NI_MAXHOST); + /* We have a non-null-terminated ASCII string stored in WORDs + * presumingly little-endian way because we are on Windows. */ + if (result) + { + for (i = 1; i < result; ++i) + idn_hostname[i] = idn_hostname[i * 2]; + idn_hostname[i] = 0; + } + else + { + free(idn_hostname); + idn_hostname = NULL; + } + } #endif error_code = getaddrinfo(idn_hostname ? idn_hostname : hostname, port_string, &hints, &res0); From aa5b5c7d7a7b970647e590f17754e95a6cdbf9c4 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Fri, 20 Sep 2024 18:39:30 -0500 Subject: [PATCH 07/14] GitHub actions test for Windows native libs --- .github/workflows/windows.yml | 96 +++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cf85fc8..620b834 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -3,7 +3,7 @@ name: Windows build on: [push, pull_request] jobs: - build: + libressl: runs-on: windows-latest defaults: run: @@ -31,17 +31,103 @@ jobs: make make install-strip DESTDIR=/d/a/msmtp/tmp cp `ldd src/msmtp | awk '/\/ucrt64/ {print $3}' | uniq` /d/a/msmtp/tmp/bin/ + - name: Artifacts + uses: actions/upload-artifact@v4 + with: + name: msmtp-libressl + path: D:\a\msmtp\tmp\ + native: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Install build dependencies + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + pkgconf + automake + autoconf + make + texinfo + - name: Configure + run: | + autoreconf -i + # The only function from libwinpthread is clock_gettime() + ./configure --with-tls=sspi --disable-nls LDFLAGS=-Wl,-Bstatic,-lwinpthread + - name: Build + run: | + make + strip --strip-all src/msmtp.exe + - name: List SO imports + run: | + ldd src/msmtp + src/msmtp --version + - name: Test DNS SRV record lookup + continue-on-error: true + run: | + src/msmtp --configure user@gmail.com | tee output.log + grep -q 'port 587' output.log && echo ๐ŸŽ‰ Success + - name: Get server info + continue-on-error: true + run: | + src/msmtp --serverinfo --tls --host=smtp.gmail.com --port=587 | tee output.log + grep -q 'ESMTP' output.log && echo ๐ŸŽ‰ Success + - name: Limit to TLSv1.2 + continue-on-error: true + run: | + src/msmtp --serverinfo --tls-priorities=PROTOCOLS=TLSv1.2 --tls --host=smtp.gmail.com --port=587 | tee output.log + grep -q 'TLS1\.2' output.log && echo ๐ŸŽ‰ Success + - name: Test with wrong host on certificate + continue-on-error: true + run: | + src/msmtp --serverinfo --tls --tls-starttls=off --host=wrong.host.badssl.com --port=443 2>&1 | tee output.log || true + grep -q 'the certificate owner does not match hostname' output.log && echo ๐ŸŽ‰ Success + - name: Test with revoked certificate + continue-on-error: true + run: | + src/msmtp --serverinfo --tls --tls-starttls=off --host=revoked.badssl.com --port=443 2>&1 | tee output.log || true + grep -q 'revoked' output.log && echo ๐ŸŽ‰ Success + - name: Test with expired certificate + continue-on-error: true + run: | + src/msmtp --serverinfo --tls --tls-starttls=off --host=expired.badssl.com --port=443 2>&1 | tee output.log || true + grep -q 'expired' output.log && echo ๐ŸŽ‰ Success + - name: Test with self-signed certificate + continue-on-error: true + run: | + src/msmtp --serverinfo --tls --tls-starttls=off --host=self-signed.badssl.com --port=443 2>&1 | tee output.log || true + grep -q 'not trusted' output.log && echo ๐ŸŽ‰ Success + - name: Test with non-secure protocol + continue-on-error: true + run: src/msmtp --serverinfo --tls --tls-starttls=off --host=dh480.badssl.com --port=443 + - name: Set locale + shell: pwsh + run: | + Set-WinSystemLocale -SystemLocale ru-RU + - name: Test IDN + continue-on-error: true + run: | + src/msmtp --debug --serverinfo --tls --host=mx3.ะฟะพั‡ั‚ะฐ.ั€ัƒั --port=1234 > output.log 2>&1 || true + cat output.log | iconv -f cp1251 -t utf-8 + grep -q 'connection refused' output.log && echo ๐ŸŽ‰ The host was found successfully but the connection was refused. - name: Upload release to GitHub if: github.ref_type == 'tag' shell: pwsh env: GH_TOKEN: ${{ github.token }} run: | - 7z a msmtp.zip D:\a\msmtp\tmp\* + 7z a msmtp-native.zip src\msmtp.exe gh release create ${{ github.ref_name }} --generate-notes || echo "Release already exists!" - gh release upload ${{ github.ref_name }} msmtp.zip + gh release upload ${{ github.ref_name }} msmtp-native.zip#Windows native support for DNS-based configufation, IDN, and Schannel SSP for TLS - name: Artifacts uses: actions/upload-artifact@v4 with: - name: msmtp - path: D:\a\msmtp\tmp\ + name: msmtp-native + path: src/msmtp.exe From 692a623a0864e5d0947dc797ec96365b855238d7 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Mon, 23 Sep 2024 13:35:30 -0500 Subject: [PATCH 08/14] fixup! Windows native IDN --- src/msmtp.c | 2 +- src/net.c | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/msmtp.c b/src/msmtp.c index 8884954..c5a9d15 100644 --- a/src/msmtp.c +++ b/src/msmtp.c @@ -3763,7 +3763,7 @@ void msmtp_print_conf(msmtp_cmdline_conf_t conf, account_t *account) int main(int argc, char *argv[]) { -#if defined(W32_NATIVE) && !defined(_UNICODE) && !defined(HAVE_LIBIDN)// && !defined(ENABLE_NLS) +#if defined(W32_NATIVE) && !defined(_UNICODE) && !defined(HAVE_LIBIDN) && !defined(ENABLE_NLS) /* We need this for proper IDN conversion later on using Windows native way * AND proper console output. * > At program startup, the equivalent of the following statement is executed : diff --git a/src/net.c b/src/net.c index 04bc8bc..e1dedff 100644 --- a/src/net.c +++ b/src/net.c @@ -765,12 +765,14 @@ int net_open_socket( /* FIXME: Something messes up locale (at least on MSYS2/UCRT64) and setlocale(LC_ALL, "") won't fix it. * The commented code below is the way to get proper console output for hostname. * Otherwise let's make sure we get proper IDN conversion and that is it. */ + /* char locale_name[LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)]; GetSystemDefaultLocaleName((LPWSTR)locale_name, LOCALE_NAME_MAX_LENGTH); size_t len = wcsnlen_s((LPWSTR)locale_name, LOCALE_NAME_MAX_LENGTH); for (int i = 1; i <= len; ++i) locale_name[i] = locale_name[i * 2]; setlocale(LC_ALL, locale_name); + */ _locale_t locale = _create_locale(LC_ALL, ""); errno_t err = _mbstowcs_s_l(&conv, hostname_wide, ARRAYSIZE(hostname_wide), hostname, _TRUNCATE, locale); _free_locale(locale); From c1bfab0125286f8cc3f0b562ecf5b910fb890def Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Mon, 23 Sep 2024 14:51:55 -0500 Subject: [PATCH 09/14] Test IDN with NLS on GH --- .github/workflows/windows.yml | 48 +++++++++++++++++++++++++++++++++++ src/net.c | 5 ++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 620b834..7c81020 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -36,6 +36,54 @@ jobs: with: name: msmtp-libressl path: D:\a\msmtp\tmp\ + nls: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Install build dependencies + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + pkgconf + automake + autoconf + make + texinfo + - name: Build + run: | + autoreconf -i + mkdir nls && cd nls + # The only function from libwinpthread is clock_gettime() + ../configure --with-tls=sspi LDFLAGS=-Wl,-Bstatic,-lwinpthread CFLAGS=-DFIXME1 + make + cd .. && mkdir norm && cd norm + ../configure --with-tls=sspi LDFLAGS=-Wl,-Bstatic,-lwinpthread + make + - name: Set locale + shell: pwsh + run: | + Set-WinSystemLocale -SystemLocale ru-RU + - name: Test IDN fixme + continue-on-error: true + run: | + nls/src/msmtp --debug --serverinfo --tls --host=mx3.ะฟะพั‡ั‚ะฐ.ั€ัƒั --port=1234 > output.log 2>&1 || true + cat output.log | iconv -f cp1251 -t utf-8 + grep -q 'connection refused' output.log && echo ๐ŸŽ‰ The host was found successfully but the connection was refused. + - name: Test IDN fixme 2 + continue-on-error: true + run: | + nls/src/msmtp --debug --serverinfo --tls --host=mx3.ะฟะพั‡ั‚ะฐ.ั€ัƒั --port=1234 + - name: Test IDN norm 2 + continue-on-error: true + run: | + norm/src/msmtp --debug --serverinfo --tls --host=mx3.ะฟะพั‡ั‚ะฐ.ั€ัƒั --port=1234 native: runs-on: windows-latest defaults: diff --git a/src/net.c b/src/net.c index e1dedff..0e88ff5 100644 --- a/src/net.c +++ b/src/net.c @@ -761,22 +761,21 @@ int net_open_socket( size_t conv; WCHAR hostname_wide[NI_MAXHOST]; /* We rely on a proper setlocale earlier that was NOT "C" as set default by MS C runtime. */ -#ifdef ENABLE_NLS +#if defined(ENABLE_NLS) && defined(FIXME1) /* FIXME: Something messes up locale (at least on MSYS2/UCRT64) and setlocale(LC_ALL, "") won't fix it. * The commented code below is the way to get proper console output for hostname. * Otherwise let's make sure we get proper IDN conversion and that is it. */ - /* char locale_name[LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)]; GetSystemDefaultLocaleName((LPWSTR)locale_name, LOCALE_NAME_MAX_LENGTH); size_t len = wcsnlen_s((LPWSTR)locale_name, LOCALE_NAME_MAX_LENGTH); for (int i = 1; i <= len; ++i) locale_name[i] = locale_name[i * 2]; setlocale(LC_ALL, locale_name); - */ _locale_t locale = _create_locale(LC_ALL, ""); errno_t err = _mbstowcs_s_l(&conv, hostname_wide, ARRAYSIZE(hostname_wide), hostname, _TRUNCATE, locale); _free_locale(locale); #else + setlocale(LC_ALL, ""); errno_t err = mbstowcs_s(&conv, hostname_wide, ARRAYSIZE(hostname_wide), hostname, _TRUNCATE); #endif if (!err) From 877c2b9e12564156e2c3065d01a3e90c3daca02c Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 25 Sep 2024 09:19:58 -0500 Subject: [PATCH 10/14] Optionally use Windows Credential Manager --- src/msmtp.c | 6 +++- src/password.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/msmtp.c b/src/msmtp.c index c5a9d15..e657a16 100644 --- a/src/msmtp.c +++ b/src/msmtp.c @@ -1749,6 +1749,10 @@ int msmtp_configure(const char *address, const char *conffile) tmpstr = xasprintf("security add-internet-password -s %s -r smtp -a %s -w", hostname, local_part); printf("# - %s\n# %s\n", _("add your password to the key ring:"), tmpstr); free(tmpstr); +#elif defined USE_CREDMAN + tmpstr = xasprintf("cmdkey /add:" PACKAGE_NAME "_%s /user:%s /pass", hostname, address); + printf("# - %s\n# %s\n", _("add your password to the key ring:"), tmpstr); + free(tmpstr); #else printf("# - %s\n# %s\n", _("encrypt your password:"), "gpg -e -o ~/.msmtp-password.gpg"); #endif @@ -1761,7 +1765,7 @@ int msmtp_configure(const char *address, const char *conffile) printf("tls_starttls %s\n", starttls ? "on" : "off"); printf("auth on\n"); printf("user %s\n", local_part); -#if !defined HAVE_LIBSECRET && !defined HAVE_MACOSXKEYRING +#if !defined HAVE_LIBSECRET && !defined HAVE_MACOSXKEYRING && !defined USE_CREDMAN printf("passwordeval gpg --no-tty -q -d ~/.msmtp-password.gpg\n"); #endif printf("from %s\n", address); diff --git a/src/password.c b/src/password.c index b4a27ae..559cb5b 100644 --- a/src/password.c +++ b/src/password.c @@ -28,11 +28,19 @@ #include #include #include -#ifdef HAVE_LIBSECRET +#if defined HAVE_LIBSECRET # include -#endif -#ifdef HAVE_MACOSXKEYRING +#elif defined HAVE_MACOSXKEYRING # include +#elif defined USE_CREDMAN +# define WIN32_LEAN_AND_MEAN +# include +# include +# include +# ifndef CRED_PACK_GENERIC_CREDENTIALS +# define CRED_PACK_GENERIC_CREDENTIALS 0x4 +# endif +# pragma comment (lib, "credui.lib") #endif #include "gettext.h" @@ -147,6 +155,89 @@ char *password_get(const char *hostname, const char *user, } #endif /* HAVE_MACOSXKEYRING */ +#ifdef USE_CREDMAN + if (!password) + { + PCREDENTIAL cred = NULL; + LPVOID buf = NULL; + ULONG buf_len; + char target[1025 /* NI_MAXHOST */ + sizeof(PACKAGE_NAME) + 1] = PACKAGE_NAME "_"; + strcat(target, hostname); + BOOL success = CredRead(target, CRED_TYPE_GENERIC, 0, &cred); + if (success) + { + buf = cred->CredentialBlob; + buf_len = cred->CredentialBlobSize; + } + else + { + DWORD err = GetLastError(); + if (err == ERROR_NOT_FOUND) + { + wchar_t caption[CRED_MAX_STRING_LENGTH]; + wsprintfW(caption, L"Password for %S at %S", user, hostname); + + /* Only -W works https://stackoverflow.com/a/25896444/673826 */ + CREDUI_INFOW ciw = { + .cbSize = sizeof(CREDUI_INFOW), + .pszCaptionText = caption, + .pszMessageText = L"The user name prefix does not matter." + "Neither does the actual user name. Leave it as is.\n" + "Only one credential per host name can be stored this way.\n" + "Although your password is stored encrypted, any application you run can read that password.\n" + "You can find saved credentials in Credential Manager with " PACKAGE_NAME "_ prefix." + }; + wchar_t user_wide[CRED_MAX_STRING_LENGTH]; + mbstowcs(user_wide, user, CRED_MAX_STRING_LENGTH); + + char bufin[CRED_MAX_CREDENTIAL_BLOB_SIZE]; + DWORD inlen = CRED_MAX_CREDENTIAL_BLOB_SIZE; + success = CredPackAuthenticationBufferW(CRED_PACK_GENERIC_CREDENTIALS, user_wide, L"", bufin, &inlen); + if (success) + { + ULONG auth_package = 0; + BOOL should_save = 1; + err = CredUIPromptForWindowsCredentialsW(&ciw, 0, &auth_package, bufin, inlen, &buf, &buf_len, &should_save, CREDUIWIN_GENERIC | CREDUIWIN_CHECKBOX); + if (err == ERROR_SUCCESS) + { + CREDENTIAL c = { + .Type = CRED_TYPE_GENERIC, + .TargetName = target, + .Persist = CRED_PERSIST_ENTERPRISE * should_save, + .CredentialBlob = buf, + .CredentialBlobSize = buf_len, + .UserName = (LPSTR)user + }; + CredWrite(&c, 0); + } + } + } + } + if (buf) + { + wchar_t name[CRED_MAX_USERNAME_LENGTH]; + wchar_t pass[CRED_MAX_STRING_LENGTH]; + wchar_t domain[CRED_MAX_STRING_LENGTH]; /* not used, will be empty */ + DWORD name_size = CRED_MAX_USERNAME_LENGTH, pass_size = CRED_MAX_STRING_LENGTH, + domain_size = CRED_MAX_STRING_LENGTH; + success = CredUnPackAuthenticationBufferW(0, buf, buf_len, name, &name_size, domain, &domain_size, pass, &pass_size); + if (success) + { + size_t len = wcslen(pass) + 1; + password = xmalloc(len); + wcstombs(password, pass, len); + } + } + if (cred) + CredFree(cred); + else if (buf) + { + SecureZeroMemory(buf, buf_len); + CoTaskMemFree(buf); + } + } +#endif + if (!password && consult_netrc) { char *netrc_directory; From 8edb616b6d40eebcdfb8951deb97c4d312ed62ee Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 25 Sep 2024 09:27:32 -0500 Subject: [PATCH 11/14] Adjust configure.ac for new --with-vault --- configure.ac | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/configure.ac b/configure.ac index b7d7b74..8b7a941 100644 --- a/configure.ac +++ b/configure.ac @@ -305,11 +305,11 @@ if test "$libidn" != "no"; then fi fi -dnl libsecret support (requires pkg-config). -AC_ARG_WITH([libsecret], [AS_HELP_STRING([--with-libsecret], - [Support libsecret (GNOME password management)])], - [libsecret=$withval],[libsecret=yes]) -if test "$libsecret" != "no"; then +AC_ARG_WITH([vault], [AS_HELP_STRING([--with-vault=libsecret|macosx-keyring|credman|no], + [Password vault support: libsecret (GNOME password management), Mac OS X Keyring, Windows Credential Manager, or none.])], + [vault=$withval], [vault=no]) +AS_CASE([$vault], +[libsecret], [ PKG_CHECK_MODULES([libsecret], [libsecret-1], [HAVE_LIBSECRET=1], [HAVE_LIBSECRET=0]) if test "$HAVE_LIBSECRET" != "1"; then AC_MSG_WARN([library libsecret not found:]) @@ -320,13 +320,7 @@ if test "$libsecret" != "no"; then libsecret="yes" AC_DEFINE([HAVE_LIBSECRET], [1], [Define to 1 if libsecret is available]) fi -fi - -dnl MacOS X Keychain Services (Security Framework) -AC_ARG_WITH([macosx-keyring], [AS_HELP_STRING([--with-macosx-keyring], - [Support Mac OS X Keyring])], - [macosx_keyring=$withval],[macosx_keyring=yes]) -if test "$macosx_keyring" != "no"; then +], [macosx_keyring], [ AC_CACHE_CHECK([for SecKeychainGetVersion], ac_cv_func_SecKeychainGetVersion, [ac_save_LIBS="$LIBS" @@ -341,7 +335,17 @@ if test "$macosx_keyring" != "no"; then else macosx_keyring=no fi -fi +], [credman], [ + credman=yes + LIBS="$LIBS -lcredui -lole32" + AC_DEFINE([USE_CREDMAN], [1], [Define to 1 if you want to use Windows Credential Manager]) +], [no], [ + libsecret="no" + macosx_keyring=no + credman=no +], + [AC_MSG_ERROR([Invalid --with-vault=$withval. Use --with-vault=libsecret|macosx-keyring|credman|no])] +) dnl Check if msmtpd should be built AC_ARG_WITH([msmtpd], @@ -372,6 +376,5 @@ else echo "IDN support ............ : no" fi echo "GNU SASL support ....... : $libgsasl" -echo "Libsecret support (GNOME): $libsecret" -echo "MacOS X Keychain support : $macosx_keyring" -echo "Build msmtpd ............: $build_msmtpd" +echo "Password vault ......... : $vault" +echo "Build msmtpd ........... : $build_msmtpd" From 59ad71800870133d5e0c3a7797242ffb879c4727 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 25 Sep 2024 09:29:36 -0500 Subject: [PATCH 12/14] Build with Windows Credential Manager on GH Actions --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 7c81020..c66d9c9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -108,7 +108,7 @@ jobs: run: | autoreconf -i # The only function from libwinpthread is clock_gettime() - ./configure --with-tls=sspi --disable-nls LDFLAGS=-Wl,-Bstatic,-lwinpthread + ./configure --with-tls=sspi --with-vault=credman --disable-nls LDFLAGS=-Wl,-Bstatic,-lwinpthread - name: Build run: | make From 1af837b51dabc3ec465dee7ff83d2ffa8e6d74b4 Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 25 Sep 2024 09:35:30 -0500 Subject: [PATCH 13/14] fixup! Adjust configure.ac for new --with-vault --- configure.ac | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 8b7a941..b9923ad 100644 --- a/configure.ac +++ b/configure.ac @@ -307,7 +307,7 @@ fi AC_ARG_WITH([vault], [AS_HELP_STRING([--with-vault=libsecret|macosx-keyring|credman|no], [Password vault support: libsecret (GNOME password management), Mac OS X Keyring, Windows Credential Manager, or none.])], - [vault=$withval], [vault=no]) + [vault=$withval], [vault=none]) AS_CASE([$vault], [libsecret], [ PKG_CHECK_MODULES([libsecret], [libsecret-1], [HAVE_LIBSECRET=1], [HAVE_LIBSECRET=0]) @@ -339,12 +339,12 @@ AS_CASE([$vault], credman=yes LIBS="$LIBS -lcredui -lole32" AC_DEFINE([USE_CREDMAN], [1], [Define to 1 if you want to use Windows Credential Manager]) -], [no], [ +], [none], [ libsecret="no" macosx_keyring=no credman=no ], - [AC_MSG_ERROR([Invalid --with-vault=$withval. Use --with-vault=libsecret|macosx-keyring|credman|no])] + [AC_MSG_ERROR([Invalid --with-vault=$vault. Use --with-vault=libsecret|macosx-keyring|credman|no])] ) dnl Check if msmtpd should be built From 70d09b60a26e734a01cf580df30f401e08a3e20b Mon Sep 17 00:00:00 2001 From: Mikhail Titov Date: Wed, 25 Sep 2024 09:50:15 -0500 Subject: [PATCH 14/14] fixup! Optionally use Windows Credential Manager --- src/msmtp.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/msmtp.c b/src/msmtp.c index e657a16..4410444 100644 --- a/src/msmtp.c +++ b/src/msmtp.c @@ -2245,15 +2245,14 @@ void msmtp_print_version(void) #endif printf("\n"); printf(_("Keyring support: ")); -#if !defined HAVE_LIBSECRET && !defined HAVE_MACOSXKEYRING - printf(_("none")); -#else -# ifdef HAVE_LIBSECRET +#if defined HAVE_LIBSECRET printf(_("Gnome ")); -# endif -# ifdef HAVE_MACOSXKEYRING +#elif defined HAVE_MACOSXKEYRING printf(_("MacOS ")); -# endif +#elif defined USE_CREDMAN + printf(_("Windows Credential Manager")); +#else + printf(_("none")); #endif printf("\n"); sysconfdir = get_sysconfdir();