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/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 fb582aab..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 @@ -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 @@ -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/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..4a76fc37 100644 --- a/doc/src/main/resources/docs/COMPAT.txt +++ b/doc/src/main/resources/docs/COMPAT.txt @@ -3,6 +3,19 @@ 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. + + 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 new file mode 100644 index 00000000..6b89715b --- /dev/null +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/smtp/SmtpStartTlsTrustTest.java @@ -0,0 +1,204 @@ +/* + * 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.eclipse.angus.mail.test.TestServer; +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(TestServer.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 + 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("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/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); 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