From 10f3ef4f7077b70851bf852ed7a917b8901d5871 Mon Sep 17 00:00:00 2001 From: Marcel May Date: Thu, 2 Nov 2023 16:07:59 +0100 Subject: [PATCH] Support configuring key password for custom TLS certificate ( fixes #615 ) --- docs/index.html | 10 ++ .../util/DummySSLServerSocketFactory.java | 64 +++++----- .../DummySSLServerSocketFactoryTest.java | 111 ++++++++++++++---- 3 files changed, 130 insertions(+), 55 deletions(-) diff --git a/docs/index.html b/docs/index.html index 0fbcdaeb08..7ed10b9236 100644 --- a/docs/index.html +++ b/docs/index.html @@ -775,6 +775,16 @@

Starting GreenMail standalone

+ + -Dgreenmail.tls.key.password
Requires 1.6.15/2.0.1/2.1.0-alpha-3 + Configures a PKCS12 key password. Defaults to keystore password. +

+

+ Example and default used in GreenMail container image: + -Dgreenmail.tls.key.password=changeit4key +
+ + -Dgreenmail.auth.disabled Disables user authentication check, so that any password works. diff --git a/greenmail-core/src/main/java/com/icegreen/greenmail/util/DummySSLServerSocketFactory.java b/greenmail-core/src/main/java/com/icegreen/greenmail/util/DummySSLServerSocketFactory.java index f55fbd8876..18a0da3610 100644 --- a/greenmail-core/src/main/java/com/icegreen/greenmail/util/DummySSLServerSocketFactory.java +++ b/greenmail-core/src/main/java/com/icegreen/greenmail/util/DummySSLServerSocketFactory.java @@ -8,12 +8,7 @@ import org.slf4j.LoggerFactory; import javax.net.ServerSocketFactory; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.SSLServerSocketFactory; -import javax.net.ssl.TrustManager; +import javax.net.ssl.*; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -24,7 +19,6 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; - /** * DummySSLServerSocketFactory - NOT SECURE *

@@ -38,7 +32,11 @@ *

* The system property {@value #GREENMAIL_KEYSTORE_PASSWORD_PROPERTY} can override the default keystore password. *

- * GreenMail provides the keystore resource. For customization, place your greenmail.p12 before greenmail JAR in the classpath. + * The system property {@value #GREENMAIL_KEY_PASSWORD_PROPERTY} can override the default key password + * (defaults to keystore password). + *

+ * GreenMail provides the keystore resource. For customization, place your greenmail.p12 before + * greenmail JAR in the classpath. * * @author Wael Chatila * @since Feb 2006 @@ -47,6 +45,7 @@ public class DummySSLServerSocketFactory extends SSLServerSocketFactory { protected final Logger log = LoggerFactory.getLogger(DummySSLServerSocketFactory.class); public static final String GREENMAIL_KEYSTORE_FILE_PROPERTY = "greenmail.tls.keystore.file"; public static final String GREENMAIL_KEYSTORE_PASSWORD_PROPERTY = "greenmail.tls.keystore.password"; + public static final String GREENMAIL_KEY_PASSWORD_PROPERTY = "greenmail.tls.key.password"; public static final String GREENMAIL_KEYSTORE_P12 = "greenmail.p12"; public static final String GREENMAIL_KEYSTORE_JKS = "greenmail.jks"; private final SSLServerSocketFactory factory; @@ -54,21 +53,20 @@ public class DummySSLServerSocketFactory extends SSLServerSocketFactory { // From https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html#SupportedCipherSuites static final String[] ANONYMOUS_CIPHERS = { - "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA" - , "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5" - , "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA" - , "SSL_DH_anon_WITH_DES_CBC_SHA" - , "SSL_DH_anon_WITH_RC4_128_MD5" - , "TLS_DH_anon_WITH_AES_128_CBC_SHA" - , "TLS_DH_anon_WITH_AES_128_CBC_SHA256" - , "TLS_DH_anon_WITH_AES_256_CBC_SHA" - , "TLS_DH_anon_WITH_AES_256_CBC_SHA256" - , "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA" - , "TLS_ECDH_anon_WITH_AES_128_CBC_SHA" - , "TLS_ECDH_anon_WITH_AES_256_CBC_SHA" - , "TLS_ECDH_anon_WITH_NULL_SHA" - , "TLS_ECDH_anon_WITH_RC4_128_SHA" - }; + "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", + "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", + "SSL_DH_anon_WITH_DES_CBC_SHA", + "SSL_DH_anon_WITH_RC4_128_MD5", + "TLS_DH_anon_WITH_AES_128_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + "TLS_ECDH_anon_WITH_NULL_SHA", + "TLS_ECDH_anon_WITH_RC4_128_SHA"}; public DummySSLServerSocketFactory() { try { @@ -76,13 +74,16 @@ public DummySSLServerSocketFactory() { String defaultAlg = KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory km = KeyManagerFactory.getInstance(defaultAlg); ks = KeyStore.getInstance(KeyStore.getDefaultType()); - char[] pass = System.getProperty(GREENMAIL_KEYSTORE_PASSWORD_PROPERTY,"changeit").toCharArray(); + + char[] pass = System.getProperty(GREENMAIL_KEYSTORE_PASSWORD_PROPERTY, "changeit").toCharArray(); loadKeyStore(pass); - km.init(ks, pass); + + String keyPassStr = System.getProperty(GREENMAIL_KEY_PASSWORD_PROPERTY); + char[] keyPass = keyPassStr != null ? keyPassStr.toCharArray() : pass; + km.init(ks, keyPass); + KeyManager[] kma = km.getKeyManagers(); - sslcontext.init(kma, - new TrustManager[]{new DummyTrustManager()}, - null); + sslcontext.init(kma, new TrustManager[]{new DummyTrustManager()}, null); factory = sslcontext.getServerSocketFactory(); } catch (Exception e) { throw new IllegalStateException("Can not create and initialize SSL", e); @@ -110,8 +111,8 @@ private void loadKeyStore(KeyStore keyStore, char[] pass, String keystoreResourc keyStore.load(is, pass); } catch (IOException ex) { // Try hard coded default keystore - throw new IllegalStateException("Can not load greenmail keystore from '" + keystoreResource + - "' in classpath", ex); + throw new IllegalStateException( + "Can not load greenmail keystore from '" + keystoreResource + "' in classpath", ex); } } @@ -122,8 +123,7 @@ private void loadKeyStore(KeyStore keyStore, char[] pass, File keystoreResource) keyStore.load(is, pass); } catch (IOException ex) { // Try hard coded default keystore - throw new IllegalStateException( - "Can not load greenmail keystore from file '" + keystoreResource + "'", ex); + throw new IllegalStateException("Can not load greenmail keystore from file '" + keystoreResource + "'", ex); } } diff --git a/greenmail-core/src/test/java/com/icegreen/greenmail/DummySSLServerSocketFactoryTest.java b/greenmail-core/src/test/java/com/icegreen/greenmail/DummySSLServerSocketFactoryTest.java index 6ff1f17d13..6da9e36ef7 100644 --- a/greenmail-core/src/test/java/com/icegreen/greenmail/DummySSLServerSocketFactoryTest.java +++ b/greenmail-core/src/test/java/com/icegreen/greenmail/DummySSLServerSocketFactoryTest.java @@ -1,16 +1,19 @@ package com.icegreen.greenmail; import com.icegreen.greenmail.util.DummySSLServerSocketFactory; +import org.junit.After; import org.junit.Test; +import sun.security.x509.*; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; +import java.math.BigInteger; +import java.security.*; import java.security.cert.Certificate; -import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; import static org.assertj.core.api.Assertions.assertThat; @@ -23,44 +26,106 @@ public void testLoadDefaultKeyStore() throws KeyStoreException { } @Test - public void testLoadKeyStoreViaSystemProperty() throws KeyStoreException, CertificateException, - IOException, NoSuchAlgorithmException { - // Load default KS, as re-using an existing cert is easier than creating one for testing - KeyStore systemKs = KeyStore.getInstance(KeyStore.getDefaultType()); - try (final FileInputStream stream = new FileInputStream( - System.getProperty("java.home") + "/lib/security/cacerts")) { - systemKs.load(stream, "changeit".toCharArray()); - } + public void testLoadKeyStoreViaSystemPropertyWithDefaultKeyPwd() + throws GeneralSecurityException, IOException { + testLoadKeyStoreViaSystemProperty("store password", null); + } + + @Test + public void testLoadKeyStoreViaSystemPropertyWithProvidedKeyPwd() + throws GeneralSecurityException, IOException { + testLoadKeyStoreViaSystemProperty("store password", "key password"); + } + + @After + public void cleanup() { + System.clearProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_FILE_PROPERTY); + System.clearProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_PASSWORD_PROPERTY); + System.clearProperty(DummySSLServerSocketFactory.GREENMAIL_KEY_PASSWORD_PROPERTY); + } + public void testLoadKeyStoreViaSystemProperty(String storePassword, String keyPassword) + throws GeneralSecurityException, IOException { // Prepare new keystore KeyStore testKs = KeyStore.getInstance(KeyStore.getDefaultType()); testKs.load(null, null); // Initialize - // Create dummy entry - String testAlias = "greenmail-testLoadKeyStoreViaSystemProperty-alias"; - String baseAlias = systemKs.aliases().nextElement(); // Any alias is fine - final Certificate testCert = systemKs.getCertificate(baseAlias); - assertThat(testCert).isNotNull(); - testKs.setCertificateEntry(testAlias, testCert); + // Create key and certificate + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(4096); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + String dn = "CN=greenmail.test"; + final X509Certificate cert = generateCertificate(dn, keyPair, 1, AlgorithmId.get("SHA256WithRSA")); + final String alias = "test-key"; + testKs.setKeyEntry(alias, keyPair.getPrivate(), + (null != keyPassword ? keyPassword : storePassword).toCharArray(), + new Certificate[]{cert}); // Save to file - String password = "some password"; final String filename = "testLoadKeyStoreViaSystemProperty." + testKs.getType(); try (FileOutputStream fos = new FileOutputStream(filename)) { - testKs.store(fos, password.toCharArray()); + testKs.store(fos, storePassword.toCharArray()); } try { System.setProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_FILE_PROPERTY, filename); - System.setProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_PASSWORD_PROPERTY, password); + System.setProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_PASSWORD_PROPERTY, storePassword); + if (null != keyPassword) { + System.setProperty(DummySSLServerSocketFactory.GREENMAIL_KEY_PASSWORD_PROPERTY, keyPassword); + } // Check if loaded DummySSLServerSocketFactory factory = new DummySSLServerSocketFactory(); KeyStore ks = factory.getKeyStore(); - assertThat(ks.containsAlias(testAlias)).isTrue(); + assertThat(ks.containsAlias(alias)).isTrue(); } finally { System.clearProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_FILE_PROPERTY); System.clearProperty(DummySSLServerSocketFactory.GREENMAIL_KEYSTORE_PASSWORD_PROPERTY); } } + + /** + * Create a self-signed X.509 certificate + *

+ * Based on https://stackoverflow.com/questions/1615871 + * + * @param dn the X.509 Distinguished Name + * @param pair the KeyPair + * @param days how many days till expiration + * @param algo the signing algorithm, eg "SHA256WithRSA" + */ + public X509Certificate generateCertificate(String dn, KeyPair pair, int days, AlgorithmId algo) + throws GeneralSecurityException, IOException { + PrivateKey privateKey = pair.getPrivate(); + X509CertInfo info = new X509CertInfo(); + + Instant now = Instant.now(); + CertificateValidity interval = new CertificateValidity( + Date.from(now), + Date.from(now.plus(Duration.ofDays(days))) + ); + + X500Name owner = new X500Name(dn); + + info.set(X509CertInfo.VALIDITY, interval); + info.set(X509CertInfo.SERIAL_NUMBER, + new CertificateSerialNumber(new BigInteger(64, new SecureRandom()))); + info.set(X509CertInfo.SUBJECT, owner); + info.set(X509CertInfo.ISSUER, owner); + info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic())); + info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); + info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo)); + + // Sign the cert to identify the algorithm that's used. + X509CertImpl cert = new X509CertImpl(info); + cert.sign(privateKey, algo.getName()); + + // Update the algorithm, and resign. + algo = (AlgorithmId) cert.get(X509CertImpl.SIG_ALG); + info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo); + cert = new X509CertImpl(info); + cert.sign(privateKey, algo.getName()); + return cert; + } }