From b095966b75f2c98d1b9df84faaded598950f6ad5 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Fri, 7 Nov 2025 11:27:37 +0100 Subject: [PATCH 1/3] Testing mail.smtp.ssl.trust seems to be broken #187 Signed-off-by: Jorge Bescos Gascon --- copyright-exclude | 2 + .../angus/mail/util/SocketFetcher.java | 39 +++- .../mail/smtp/SmtpStartTlsTrustTest.java | 188 ++++++++++++++++++ .../src/test/resources/keystore.jks | Bin 0 -> 2766 bytes .../angus-mail/src/test/resources/test-hosts | 2 + 5 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java create mode 100644 providers/angus-mail/src/test/resources/keystore.jks create mode 100644 providers/angus-mail/src/test/resources/test-hosts diff --git a/copyright-exclude b/copyright-exclude index 027aa78f..59e8d745 100644 --- a/copyright-exclude +++ b/copyright-exclude @@ -30,6 +30,8 @@ mailhandler/src/main/resources/META-INF/hk2-locator/default docker/JTReportParser/ providers/angus-mail/src/test/resources/org/eclipse/angus/mail/util/paramdatanostrict providers/angus-mail/src/test/resources/org/eclipse/angus/mail/util/paramdata +providers/angus-mail/src/test/resources/keystore.jks +providers/angus-mail/src/test/resources/test-hosts .txt .TXT www/ diff --git a/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java b/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java index fb582aab..b9cd3e88 100644 --- a/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java +++ b/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -638,6 +638,43 @@ private static void configureSSLSocket(Socket socket, String host, prefix + ".ssl.checkserveridentity", true) ? "LDAPS" : (String) null; SSLParameters params = sslsocket.getSSLParameters(); + /* + https://github.com/eclipse-ee4j/angus-mail/issues/187 + Setting setEndpointIdentificationAlgorithm to not null, overrides MailTrustManager#checkServerTrusted, as seen in this stacktrace. + As a result, trusted hosts are not verified when ssl.checkserveridentity = true + + Caused by: javax.net.ssl.SSLHandshakeException: No subject alternative DNS name matching mailtest.local found. + at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130) + at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:378) + at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321) + at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:316) + at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1318) + at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1195) + at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1138) + at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:393) + at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:476) + at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:447) + at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:201) + at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:172) + at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1506) + at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1421) + at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:455) + at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:426) + at org.eclipse.angus.mail.util.SocketFetcher.configureSSLSocket(SocketFetcher.java:662) + at org.eclipse.angus.mail.util.SocketFetcher.createSocket(SocketFetcher.java:409) + at org.eclipse.angus.mail.util.SocketFetcher.getSocket(SocketFetcher.java:243) + at org.eclipse.angus.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:2193) + ... 33 more + Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching mailtest.local found. + at java.base/sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:207) + at java.base/sun.security.util.HostnameChecker.match(HostnameChecker.java:103) + at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:461) + at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:417) + at java.base/sun.security.ssl.AbstractTrustManagerWrapper.checkAdditionalTrust(SSLContextImpl.java:1463) + at java.base/sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:1431) + at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1302) + ... 48 more + */ params.setEndpointIdentificationAlgorithm(eia); sslsocket.setSSLParameters(params); diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java new file mode 100644 index 00000000..ac7ddafc --- /dev/null +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + + +package org.eclipse.angus.mail.smtps; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.Locale; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; + +public class SmtpStartTlsTrustTest { + + private static final int PORT = 16025; + private static final String HOST_FILE_KEY = "jdk.net.hosts.file"; + private static String hostsFile; + private ExecutorService es; + + @BeforeClass + public static void beforeClass() { + hostsFile = System.getProperty(HOST_FILE_KEY); + if (hostsFile != null) { + System.clearProperty(HOST_FILE_KEY); + } + System.setProperty(HOST_FILE_KEY, SmtpStartTlsTrustTest.class.getResource("/test-hosts").getPath()); + } + + @AfterClass + public static void afterClass() { + if (hostsFile != null) { + System.setProperty(HOST_FILE_KEY, hostsFile); + } + } + + @Before + public void before() throws Exception { + es = Executors.newSingleThreadExecutor(); + assertEquals("127.0.0.1", InetAddress.getByName("mailtest.local").getHostAddress()); + es.execute(() -> { + try { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(SmtpStartTlsTrustTest.class.getResourceAsStream("/keystore.jks"), "changeit".toCharArray()); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "changeit".toCharArray()); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(kmf.getKeyManagers(), null, new SecureRandom()); + + SSLServerSocketFactory ssf = ctx.getServerSocketFactory(); + try (SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(PORT)) { + serverSocket.setNeedClientAuth(false); + serverSocket.setWantClientAuth(false); + try (SSLSocket socket = (SSLSocket) serverSocket.accept()) { + socket.startHandshake(); + BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII)); + BufferedReader in = new BufferedReader( + new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)); + out.write("220 mailtest.local Simple SMTP Ready\r\n"); + out.flush(); + String line; + while ((line = in.readLine()) != null) { + if (line.toUpperCase(Locale.ROOT).startsWith("EHLO") || + line.toUpperCase(Locale.ROOT).startsWith("HELO")) { + out.write("250-mailtest.local Hello\r\n"); + out.write("250 STARTTLS\r\n"); + out.flush(); + } else if (line.equalsIgnoreCase("QUIT")) { + out.write("221 Bye\r\n"); + out.flush(); + break; + } else { + out.write("250 OK\r\n"); + out.flush(); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + Thread.sleep(1000); + } + + @After + public void after() throws InterruptedException { + es.shutdown(); + assertTrue(es.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + public void testTrustAllHostsDisableServerIdentity() throws Exception { + Properties props = new Properties(); + props.put("mail.smtps.host", "mailtest.local"); + props.put("mail.smtps.port", PORT); + props.put("mail.smtps.starttls.enable", "true"); + props.put("mail.smtps.ssl.trust", "*"); + props.put("mail.smtps.ssl.checkserveridentity", "false"); + Session session = Session.getInstance(props); + Transport transport = session.getTransport("smtps"); + transport.connect(); + transport.close(); + } + + @Test + @Ignore("Reproduces issue 187") + public void testTrustAllHostsEnableServerIdentity() throws Exception { + Properties props = new Properties(); + props.put("mail.smtps.host", "mailtest.local"); + props.put("mail.smtps.port", PORT); + props.put("mail.smtps.starttls.enable", "true"); + props.put("mail.smtps.ssl.trust", "*"); + // It will check that trust=*, and it will not fail + props.put("mail.smtps.ssl.checkserveridentity", "true"); + Session session = Session.getInstance(props); + Transport transport = session.getTransport("smtps"); + transport.connect(); + transport.close(); + } + + @Test + public void testHostnameVerificationFails() throws Exception { + Properties props = new Properties(); + props.put("mail.smtps.host", "mailtest.local"); + props.put("mail.smtps.port", PORT); + props.put("mail.smtps.starttls.enable", "true"); + props.put("mail.smtps.ssl.trust", "other.domain"); + props.put("mail.smtps.ssl.checkserveridentity", "true"); + + Session session = Session.getInstance(props); + + try { + Transport transport = session.getTransport("smtps"); + transport.connect(); + fail("Expects exception"); + } catch (MessagingException e) { + e.printStackTrace(); + assertEquals(SSLHandshakeException.class, e.getCause().getClass()); + } + } +} \ No newline at end of file diff --git a/providers/angus-mail/src/test/resources/keystore.jks b/providers/angus-mail/src/test/resources/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..494709545ec85c77537b841383dc4ba59d6c869c GIT binary patch literal 2766 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 literal 0 HcmV?d00001 diff --git a/providers/angus-mail/src/test/resources/test-hosts b/providers/angus-mail/src/test/resources/test-hosts new file mode 100644 index 00000000..9b350e6a --- /dev/null +++ b/providers/angus-mail/src/test/resources/test-hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost +127.0.0.1 mailtest.local \ No newline at end of file From dbb03eb1ade6258610ec40d3fe4983c03bc4547e Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Wed, 19 Nov 2025 09:32:37 +0100 Subject: [PATCH 2/3] extends X509ExtendedTrustManager Signed-off-by: Jorge Bescos Gascon --- .../angus/mail/util/MailSSLSocketFactory.java | 115 ++++++++++++++---- .../angus/mail/util/SocketFetcher.java | 37 ------ doc/src/main/resources/docs/CHANGES.txt | 6 + doc/src/main/resources/docs/COMPAT.txt | 18 +++ .../mail/smtp/SmtpStartTlsTrustTest.java | 3 +- .../angus/mail/util/SocketFetcherTest.java | 70 ++--------- .../mail/util/WriteTimeoutSocketTest.java | 5 +- 7 files changed, 134 insertions(+), 120 deletions(-) diff --git a/core/src/main/java/org/eclipse/angus/mail/util/MailSSLSocketFactory.java b/core/src/main/java/org/eclipse/angus/mail/util/MailSSLSocketFactory.java index 1bbe8d66..486e93f6 100644 --- a/core/src/main/java/org/eclipse/angus/mail/util/MailSSLSocketFactory.java +++ b/core/src/main/java/org/eclipse/angus/mail/util/MailSSLSocketFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -18,10 +18,12 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.net.InetAddress; @@ -334,51 +336,120 @@ public synchronized Socket createSocket(String s, int i) * * @author Stephan Sann */ - private class MailTrustManager implements X509TrustManager { + private class MailTrustManager extends X509ExtendedTrustManager { /** - * A TrustManager to pass method calls to + * A TrustManager to delegate to */ - private X509TrustManager adapteeTrustManager = null; + private final X509ExtendedTrustManager adapteeTrustManager; - /** - * Initializes a new TrustManager instance. - */ private MailTrustManager() throws GeneralSecurityException { TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init((KeyStore) null); - adapteeTrustManager = (X509TrustManager) tmf.getTrustManagers()[0]; + + TrustManager tm = tmf.getTrustManagers()[0]; + + if (tm instanceof X509ExtendedTrustManager) { + adapteeTrustManager = (X509ExtendedTrustManager) tm; + } else { + // Should not happen, but wrap X509TrustManager if needed + adapteeTrustManager = new X509ExtendedTrustManager() { + private final X509TrustManager base = (X509TrustManager) tm; + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + base.checkClientTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + base.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + base.checkServerTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + base.checkServerTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + base.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + base.checkServerTrusted(chain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return base.getAcceptedIssuers(); + } + }; + } + } + + private boolean shouldDelegate() { + return !(isTrustAllHosts() || getTrustedHosts() != null); } - /* (non-Javadoc) - * @see javax.net.ssl.X509TrustManager#checkClientTrusted( - * java.security.cert.X509Certificate[], java.lang.String) - */ @Override public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException { - if (!(isTrustAllHosts() || getTrustedHosts() != null)) + if (shouldDelegate()) adapteeTrustManager.checkClientTrusted(certs, authType); } - /* (non-Javadoc) - * @see javax.net.ssl.X509TrustManager#checkServerTrusted( - * java.security.cert.X509Certificate[], java.lang.String) - */ @Override public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException { - - if (!(isTrustAllHosts() || getTrustedHosts() != null)) + if (shouldDelegate()) adapteeTrustManager.checkServerTrusted(certs, authType); } - /* (non-Javadoc) - * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() - */ @Override public X509Certificate[] getAcceptedIssuers() { return adapteeTrustManager.getAcceptedIssuers(); } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + if (shouldDelegate()) + adapteeTrustManager.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + if (shouldDelegate()) + adapteeTrustManager.checkClientTrusted(chain, authType, engine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + if (shouldDelegate()) + adapteeTrustManager.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + if (shouldDelegate()) + adapteeTrustManager.checkServerTrusted(chain, authType, engine); + } } + } diff --git a/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java b/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java index b9cd3e88..df4044c1 100644 --- a/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java +++ b/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java @@ -638,43 +638,6 @@ private static void configureSSLSocket(Socket socket, String host, prefix + ".ssl.checkserveridentity", true) ? "LDAPS" : (String) null; SSLParameters params = sslsocket.getSSLParameters(); - /* - https://github.com/eclipse-ee4j/angus-mail/issues/187 - Setting setEndpointIdentificationAlgorithm to not null, overrides MailTrustManager#checkServerTrusted, as seen in this stacktrace. - As a result, trusted hosts are not verified when ssl.checkserveridentity = true - - Caused by: javax.net.ssl.SSLHandshakeException: No subject alternative DNS name matching mailtest.local found. - at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130) - at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:378) - at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321) - at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:316) - at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1318) - at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1195) - at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1138) - at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:393) - at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:476) - at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:447) - at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:201) - at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:172) - at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1506) - at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1421) - at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:455) - at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:426) - at org.eclipse.angus.mail.util.SocketFetcher.configureSSLSocket(SocketFetcher.java:662) - at org.eclipse.angus.mail.util.SocketFetcher.createSocket(SocketFetcher.java:409) - at org.eclipse.angus.mail.util.SocketFetcher.getSocket(SocketFetcher.java:243) - at org.eclipse.angus.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:2193) - ... 33 more - Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching mailtest.local found. - at java.base/sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:207) - at java.base/sun.security.util.HostnameChecker.match(HostnameChecker.java:103) - at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:461) - at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:417) - at java.base/sun.security.ssl.AbstractTrustManagerWrapper.checkAdditionalTrust(SSLContextImpl.java:1463) - at java.base/sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:1431) - at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1302) - ... 48 more - */ params.setEndpointIdentificationAlgorithm(eia); sslsocket.setSSLParameters(params); diff --git a/doc/src/main/resources/docs/CHANGES.txt b/doc/src/main/resources/docs/CHANGES.txt index d54cb254..bdaa4cce 100644 --- a/doc/src/main/resources/docs/CHANGES.txt +++ b/doc/src/main/resources/docs/CHANGES.txt @@ -7,6 +7,12 @@ for the Eclipse EE4J Angus Mail project: https://github.com/eclipse-ee4j/angus-mail/issues/ + CHANGES IN THE 2.1.0 RELEASE + ---------------------------- +The following bugs have been fixed in the 2.1.0 release. + +187: "mail.smtp.ssl.trust" seems to be broken + CHANGES IN THE 2.0.5 RELEASE ---------------------------- The following bugs have been fixed in the 2.0.5 release. diff --git a/doc/src/main/resources/docs/COMPAT.txt b/doc/src/main/resources/docs/COMPAT.txt index d55b0fc8..ef4ace35 100644 --- a/doc/src/main/resources/docs/COMPAT.txt +++ b/doc/src/main/resources/docs/COMPAT.txt @@ -3,6 +3,24 @@ Angus Mail ${angus-mail.version} release ------------------------------ + +-- Angus Mail 2.1.0 -- + +- MailTrustManager updated to extend X509ExtendedTrustManager + + The internal MailTrustManager implementation has been updated to extend + X509ExtendedTrustManager instead of X509TrustManager. This allows Angus Mail + to pass the connected SSLSocket or SSLEngine to the TrustManager during + certificate validation. + + Prior to this change, MailTrustManager only received the certificate chain + and authentication type. As a result, it could not participate in hostname + verification triggered by the SSLParameters endpoint identification + algorithm. + + As a result of this, setting mail..ssl.checkserveridentity = true + will check trusted hosts. + -- Angus Mail 2.0.5 -- - Resource File Duplication due to Jakartification diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java index ac7ddafc..d03a10e4 100644 --- a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java @@ -150,7 +150,6 @@ public void testTrustAllHostsDisableServerIdentity() throws Exception { } @Test - @Ignore("Reproduces issue 187") public void testTrustAllHostsEnableServerIdentity() throws Exception { Properties props = new Properties(); props.put("mail.smtps.host", "mailtest.local"); @@ -182,7 +181,7 @@ public void testHostnameVerificationFails() throws Exception { fail("Expects exception"); } catch (MessagingException e) { e.printStackTrace(); - assertEquals(SSLHandshakeException.class, e.getCause().getClass()); + assertEquals("Server is not trusted: mailtest.local", e.getCause().getMessage()); } } } \ No newline at end of file diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/SocketFetcherTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/SocketFetcherTest.java index cf861e3b..45bc03ec 100644 --- a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/SocketFetcherTest.java +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/SocketFetcherTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -534,71 +534,27 @@ public void testSSLCheckServerIdentityFalse() throws Throwable { } @Test - public void testSSLCheckServerIdentityNull() { - try { - testSSLCheckServerIdentity("localhost", (String) null); - throw new AssertionError(); - } catch (Error | RuntimeException e) { - throw e; - } catch (MessagingException me) { - Throwable cause = me.getCause(); - assertTrue(String.valueOf(cause), - cause instanceof SSLHandshakeException); - assertTrue(me.toString(), isFromTrustManager(me)); - } catch (Throwable t) { - throw new AssertionError(t); - } + public void testSSLCheckServerIdentityNull() throws Throwable { + // localhost should fail? See TestSSLSocketFactory#84 -> ((MailSSLSocketFactory) defaultFactory).setTrustedHosts("localhost"); + testSSLCheckServerIdentity("localhost", (String) null); } @Test - public void testSSLCheckServerIdentityTrue() { - try { - testSSLCheckServerIdentity("localhost", "true"); - throw new AssertionError(); - } catch (Error | RuntimeException e) { - throw e; - } catch (MessagingException me) { - Throwable cause = me.getCause(); - assertTrue(String.valueOf(cause), - cause instanceof SSLHandshakeException); - assertTrue(me.toString(), isFromTrustManager(me)); - } catch (Throwable t) { - throw new AssertionError(t); - } + public void testSSLCheckServerIdentityTrue() throws Throwable { + // localhost should fail? See TestSSLSocketFactory#84 -> ((MailSSLSocketFactory) defaultFactory).setTrustedHosts("localhost"); + testSSLCheckServerIdentity("localhost", "true"); } @Test - public void testSSLCheckServerIdentityIPv4True() { - try { - testSSLCheckServerIdentity("127.0.0.1", "true"); - throw new AssertionError(); - } catch (Error | RuntimeException e) { - throw e; - } catch (MessagingException me) { - Throwable cause = me.getCause(); - assertTrue(String.valueOf(cause), - cause instanceof SSLHandshakeException); - assertTrue(me.toString(), isFromTrustManager(me)); - } catch (Throwable t) { - throw new AssertionError(t); - } + public void testSSLCheckServerIdentityIPv4True() throws Throwable { + // localhost should fail? See TestSSLSocketFactory#84 -> ((MailSSLSocketFactory) defaultFactory).setTrustedHosts("localhost"); + testSSLCheckServerIdentity("127.0.0.1", "true"); } @Test - public void testSSLCheckServerIdentityIPv6True() { - try { - testSSLCheckServerIdentity("::1", "true"); - throw new AssertionError(); - } catch (Error | RuntimeException e) { - throw e; - } catch (MessagingException me) { - Throwable cause = me.getCause(); - assertTrue(String.valueOf(cause), - cause instanceof SSLHandshakeException); - assertTrue(me.toString(), isFromTrustManager(me)); - } catch (Throwable t) { - throw new AssertionError(t); - } + public void testSSLCheckServerIdentityIPv6True() throws Throwable { + // localhost should fail? See TestSSLSocketFactory#84 -> ((MailSSLSocketFactory) defaultFactory).setTrustedHosts("localhost"); + testSSLCheckServerIdentity("::1", "true"); } private boolean matchAnyCauseStackTrace(Throwable thrown, diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/WriteTimeoutSocketTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/WriteTimeoutSocketTest.java index 9362e75d..99c44b6a 100644 --- a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/WriteTimeoutSocketTest.java +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/WriteTimeoutSocketTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -111,8 +111,9 @@ public void testSocketFactory() throws Exception { assertTrue(sf.getSocketCreated()); } - @Test(expected = MessagingException.class) + @Test public void testSSLCheckserveridentityDefaultsTrue() throws Exception { + // Localhost should not fail if mail.imap.ssl.trust = localhost final Properties properties = new Properties(); properties.setProperty("mail.imap.host", "localhost"); properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); From c1bcc8f94388ba237b3174e9076553411026f078 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Tue, 25 Nov 2025 11:08:27 +0100 Subject: [PATCH 3/3] Addressing some review comments Signed-off-by: Jorge Bescos Gascon --- .../angus/mail/util/SocketFetcher.java | 8 ++++---- doc/src/main/resources/docs/COMPAT.txt | 5 ----- .../mail/smtp/SmtpStartTlsTrustTest.java | 19 +++++++++++++++++- .../src/test/resources/keystore.jks | Bin 2766 -> 0 bytes 4 files changed, 22 insertions(+), 10 deletions(-) delete mode 100644 providers/angus-mail/src/test/resources/keystore.jks diff --git a/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java b/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java index df4044c1..a8a11184 100644 --- a/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java +++ b/core/src/main/java/org/eclipse/angus/mail/util/SocketFetcher.java @@ -628,15 +628,15 @@ private static void configureSSLSocket(Socket socket, String host, Arrays.asList(sslsocket.getEnabledCipherSuites())); } + boolean checkServerIdentity = + PropUtil.getBooleanProperty(props, prefix + ".ssl.checkserveridentity", true); try { /* * Check server identity and trust. * See: JDK-8062515 and JDK-7192189 * LDAPS requires the same regex handling as we need */ - String eia = PropUtil.getBooleanProperty(props, - prefix + ".ssl.checkserveridentity", true) - ? "LDAPS" : (String) null; + String eia = checkServerIdentity ? "LDAPS" : (String) null; SSLParameters params = sslsocket.getSSLParameters(); params.setEndpointIdentificationAlgorithm(eia); sslsocket.setSSLParameters(params); @@ -679,7 +679,7 @@ private static void configureSSLSocket(Socket socket, String host, if (sf instanceof MailSSLSocketFactory) { MailSSLSocketFactory msf = (MailSSLSocketFactory) sf; - if (!msf.isServerTrusted(host, sslsocket)) { + if (checkServerIdentity && !msf.isServerTrusted(host, sslsocket)) { throw cleanupAndThrow(sslsocket, new IOException("Server is not trusted: " + host)); } diff --git a/doc/src/main/resources/docs/COMPAT.txt b/doc/src/main/resources/docs/COMPAT.txt index ef4ace35..4a76fc37 100644 --- a/doc/src/main/resources/docs/COMPAT.txt +++ b/doc/src/main/resources/docs/COMPAT.txt @@ -13,11 +13,6 @@ to pass the connected SSLSocket or SSLEngine to the TrustManager during certificate validation. - Prior to this change, MailTrustManager only received the certificate chain - and authentication type. As a result, it could not participate in hostname - verification triggered by the SSLParameters endpoint identification - algorithm. - As a result of this, setting mail..ssl.checkserveridentity = true will check trusted hosts. diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java index d03a10e4..6b89715b 100644 --- a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java @@ -43,6 +43,7 @@ import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; +import org.eclipse.angus.mail.test.TestServer; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -84,7 +85,7 @@ public void before() throws Exception { es.execute(() -> { try { KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(SmtpStartTlsTrustTest.class.getResourceAsStream("/keystore.jks"), "changeit".toCharArray()); + ks.load(TestServer.class.getResourceAsStream("keystore.jks"), "changeit".toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, "changeit".toCharArray()); @@ -184,4 +185,20 @@ public void testHostnameVerificationFails() throws Exception { assertEquals("Server is not trusted: mailtest.local", e.getCause().getMessage()); } } + + @Test + public void testHostnameVerificationWorks() throws Exception { + Properties props = new Properties(); + props.put("mail.smtps.host", "mailtest.local"); + props.put("mail.smtps.port", PORT); + props.put("mail.smtps.starttls.enable", "true"); + props.put("mail.smtps.ssl.trust", "other.domain"); + // Doesn't check host + props.put("mail.smtps.ssl.checkserveridentity", "false"); + + Session session = Session.getInstance(props); + Transport transport = session.getTransport("smtps"); + transport.connect(); + transport.close(); + } } \ No newline at end of file diff --git a/providers/angus-mail/src/test/resources/keystore.jks b/providers/angus-mail/src/test/resources/keystore.jks deleted file mode 100644 index 494709545ec85c77537b841383dc4ba59d6c869c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2766 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