diff options
| author | saturneric <[email protected]> | 2026-09-12 16:47:48 +0200 |
|---|---|---|
| committer | saturneric <[email protected]> | 2026-09-12 16:47:48 +0200 |
| commit | 2245ab2011b64a04c45fca2f1d231ca2a6c96362 (patch) | |
| tree | b5976e2ed051479e353f0efa96bb388875895da7 /src | |
| parent | feat(email): enforce policy-driven mail ports (diff) | |
| download | modules-2245ab2011b64a04c45fca2f1d231ca2a6c96362.tar.gz modules-2245ab2011b64a04c45fca2f1d231ca2a6c96362.zip | |
fix(email): enforce SMTP auth and certificate pinning
- require SMTP authentication when credentials are configured
- apply certificate pins to the presented leaf certificate
- preserve certificate validity and hostname verification
- correctly normalize vmime-provided SHA-256 fingerprints
- pass protocol context to TLS policy setup
- update connection test comments and formatting
Diffstat (limited to 'src')
| -rw-r--r-- | src/m_email/EMailImapWorker.cpp | 2 | ||||
| -rw-r--r-- | src/m_email/EMailSmtpWorker.cpp | 10 | ||||
| -rw-r--r-- | src/m_email/EMailTlsSetup.cpp | 75 | ||||
| -rw-r--r-- | src/m_email/EMailTlsSetup.h | 7 |
4 files changed, 70 insertions, 24 deletions
diff --git a/src/m_email/EMailImapWorker.cpp b/src/m_email/EMailImapWorker.cpp index 2405691..78041cc 100644 --- a/src/m_email/EMailImapWorker.cpp +++ b/src/m_email/EMailImapWorker.cpp @@ -183,7 +183,7 @@ void EMailImapWorker::Connect(quint64 seq, const MailAccountConfig& account, impl_->store->setTimeoutHandlerFactory(impl_->timeouts); EMailTlsSetup::Apply(impl_->session, impl_->store, - QString("store.%1").arg(protocol), config); + QString("store.%1").arg(protocol), config, true); impl_->store->setAuthenticator(vmime::make_shared<SecureAuthenticator>( config.username, password, cleartext)); diff --git a/src/m_email/EMailSmtpWorker.cpp b/src/m_email/EMailSmtpWorker.cpp index 1018471..0bfccd2 100644 --- a/src/m_email/EMailSmtpWorker.cpp +++ b/src/m_email/EMailSmtpWorker.cpp @@ -153,7 +153,7 @@ void EMailSmtpWorker::TestConnection(quint64 seq, transport->setTimeoutHandlerFactory(timeouts); EMailTlsSetup::Apply(session, transport, - QString("transport.%1").arg(protocol), config); + QString("transport.%1").arg(protocol), config, false); transport->setAuthenticator(vmime::make_shared<SecureAuthenticator>( config.username, password, cleartext)); @@ -164,8 +164,10 @@ void EMailSmtpWorker::TestConnection(quint64 seq, if (!cleartext && !EMailTlsSetup::IsSecured(transport)) { receipt.error = MailTlsRequiredError(config.host); } else { - // Reaching here means the server accepted the credentials, which is the - // whole question a connection test asks. + // Reaching here means the server completed the greeting, secured the + // link, and -- because the session demands authentication -- accepted + // the credentials. All three are required, which is the whole question + // a connection test asks. receipt.accepted = true; } @@ -233,7 +235,7 @@ void EMailSmtpWorker::Submit(quint64 seq, const MailAccountConfig& account, transport->setTimeoutHandlerFactory(timeouts); EMailTlsSetup::Apply(session, transport, - QString("transport.%1").arg(protocol), config); + QString("transport.%1").arg(protocol), config, false); transport->setAuthenticator(vmime::make_shared<SecureAuthenticator>( config.username, password, cleartext)); diff --git a/src/m_email/EMailTlsSetup.cpp b/src/m_email/EMailTlsSetup.cpp index 2955f1c..7e52aac 100644 --- a/src/m_email/EMailTlsSetup.cpp +++ b/src/m_email/EMailTlsSetup.cpp @@ -81,7 +81,7 @@ auto RootCertificates() // placeholder is left in the text and the argument is dropped with a // runtime warning. FLOG_DEBUG("loaded %1 root certificates for mail TLS", - static_cast<int>(roots.size())); + static_cast<int>(roots.size())); return roots; } @@ -99,11 +99,17 @@ auto FingerprintOf(const vmime::shared_ptr<cert::X509Certificate>& certificate) -> QString { if (!certificate) return {}; + // vmime hands back the fingerprint ALREADY FORMATTED, as the ASCII text + // "AB:CD:EF:..." rather than as the raw digest bytes + // (X509Certificate_OpenSSL.cpp:571-592). Hex-encoding that a second time -- + // which is what this used to do -- produced a string twice the right length + // that matched nothing and meant nothing, so no pin could ever apply and + // the fingerprint shown to the user was not the certificate's. const auto digest = certificate->getFingerprint(cert::X509Certificate::DIGEST_SHA256); QByteArray bytes; for (const auto byte : digest) bytes.append(static_cast<char>(byte)); - return QString::fromLatin1(bytes.toHex()).toLower(); + return QString::fromLatin1(bytes).remove(':').trimmed().toLower(); } /** @@ -118,13 +124,44 @@ auto FingerprintOf(const vmime::shared_ptr<cert::X509Certificate>& certificate) */ class RememberingVerifier : public cert::defaultCertificateVerifier { public: + /// @param pin lowercase hex SHA-256 of the one certificate this transport + /// trusts beyond the system roots, or empty for "system roots only". + explicit RememberingVerifier(QString pin) : pin_(std::move(pin)) {} + void verify(const vmime::shared_ptr<cert::certificateChain>& chain, const vmime::string& hostname) override { remember(chain); + apply_pin(chain); cert::defaultCertificateVerifier::verify(chain, hostname); } private: + QString pin_; + + /// A pin names the certificate the SERVER PRESENTS, which is the whole point + /// of pinning: the case it exists for is a self-signed or privately-issued + /// certificate, and such a certificate is by definition not in the system + /// root store. Matching the pin against the roots -- as this once did -- + /// could therefore only ever match a certificate that already verified, so + /// pinning silently did nothing at all. + /// + /// Trust is still expressed by handing the leaf to the base class as a + /// trusted certificate rather than by skipping any check, so the base + /// class's ordering continues to hold: expiry is judged BEFORE the trusted + /// comparison and the hostname AFTER it. A pinned certificate that has + /// expired, is not yet valid, or names another host still fails. That is + /// what keeps a pin from degrading into "ignore TLS errors". + void apply_pin(const vmime::shared_ptr<cert::certificateChain>& chain) { + if (pin_.isEmpty() || !chain || chain->getCount() == 0) return; + + auto leaf = vmime::dynamicCast<cert::X509Certificate>(chain->getAt(0)); + if (!leaf || FingerprintOf(leaf) != pin_) return; + + std::vector<vmime::shared_ptr<cert::X509Certificate>> trusted; + trusted.push_back(leaf); + setX509TrustedCerts(trusted); + } + static void remember(const vmime::shared_ptr<cert::certificateChain>& chain) { if (!chain || chain->getCount() == 0) return; @@ -152,11 +189,27 @@ auto ProtocolName(bool imap, MailTlsMode mode) -> QString { void Apply(const vmime::shared_ptr<vmime::net::session>& session, const vmime::shared_ptr<vmime::net::service>& service, - const QString& prefix, const MailTransportConfig& config) { + const QString& prefix, const MailTransportConfig& config, + bool imap) { if (!session || !service) return; auto& properties = session->getProperties(); + // The second load-bearing line of this file, and for a long time a missing + // one. vmime treats SMTP authentication as OPTIONAL and defaults + // "options.need-authentication" to false (SMTPServiceInfos.cpp:58). With it + // false, SMTPConnection::connect() sets m_authenticated = true and returns + // WITHOUT SENDING ANY AUTH COMMAND at all -- so every password verified, + // every password was accepted, and mail was submitted unauthenticated. + // + // IMAP has no such property and authenticates unconditionally, which is why + // this is asked for only on the SMTP side. + if (!imap && !config.username.isEmpty()) { + properties.setProperty( + QString("%1.options.need-authentication").arg(prefix).toStdString(), + true); + } + const auto tls_key = QString("%1.connection.tls").arg(prefix).toStdString(); const auto required_key = QString("%1.connection.tls.required").arg(prefix).toStdString(); @@ -176,22 +229,10 @@ void Apply(const vmime::shared_ptr<vmime::net::session>& session, // that then carries the password. Setting it makes that failure fatal. properties.setProperty(required_key, true); - auto verifier = vmime::make_shared<RememberingVerifier>(); + auto verifier = + vmime::make_shared<RememberingVerifier>(config.pinned_cert_sha256); verifier->setX509RootCAs(RootCertificates()); - if (!config.pinned_cert_sha256.isEmpty()) { - // A pin is one certificate for one transport. It is given to the base - // class as a trusted certificate, which excuses an untrusted chain and - // nothing else: expiry is checked before this is consulted and the - // hostname after it. - std::vector<vmime::shared_ptr<cert::X509Certificate>> pinned; - for (const auto& root : RootCertificates()) { - if (FingerprintOf(root) == config.pinned_cert_sha256) - pinned.push_back(root); - } - if (!pinned.empty()) verifier->setX509TrustedCerts(pinned); - } - service->setCertificateVerifier(verifier); } diff --git a/src/m_email/EMailTlsSetup.h b/src/m_email/EMailTlsSetup.h index 5719687..ed6804c 100644 --- a/src/m_email/EMailTlsSetup.h +++ b/src/m_email/EMailTlsSetup.h @@ -58,7 +58,8 @@ namespace EMailTlsSetup { /** * @brief Apply this application's security policy to a session and service. * - * Sets the transport properties that make TLS mandatory, installs the + * Sets the transport properties that make TLS mandatory, requires + * authentication where the protocol treats it as optional, installs the * certificate verifier, and applies any certificate the user has pinned for * this transport. * @@ -66,10 +67,12 @@ namespace EMailTlsSetup { * @param service the store or transport to attach the verifier to * @param prefix property prefix, e.g. "store.imaps" or "transport.smtp" * @param config the transport being connected + * @param imap true for IMAP, false for SMTP. Only SMTP treats authentication + * as optional, so only SMTP needs it demanded; see the property below. */ void Apply(const vmime::shared_ptr<vmime::net::session>& session, const vmime::shared_ptr<vmime::net::service>& service, - const QString& prefix, const MailTransportConfig& config); + const QString& prefix, const MailTransportConfig& config, bool imap); /** * @brief The vmime protocol name for a transport. |
