Skip to content

Commit

Permalink
Support configuring key password for custom TLS certificate ( fixes #615
Browse files Browse the repository at this point in the history
 )
  • Loading branch information
marcelmay committed Nov 2, 2023
1 parent 901469d commit 10f3ef4
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 55 deletions.
10 changes: 10 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,16 @@ <h4>Starting GreenMail standalone</h4>
</div>
</td>
</tr>
<tr>
<td class="text-nowrap">-Dgreenmail.tls.key.password<br><mark><b>Requires 1.6.15/2.0.1/2.1.0-alpha-3</b></mark></td>
<td>Configures a PKCS12 key password. Defaults to keystore password.
<p>
<div class="bs-example">
Example and default used in GreenMail container image:
<code>-Dgreenmail.tls.key.password=changeit4key</code>
</div>
</td>
</tr>
<tr>
<td class="text-nowrap">-Dgreenmail.auth.disabled</td>
<td>Disables user authentication check, so that any password works.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,7 +19,6 @@
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;


/**
* DummySSLServerSocketFactory - NOT SECURE
* <p/>
Expand All @@ -38,7 +32,11 @@
* <p/>
* The system property {@value #GREENMAIL_KEYSTORE_PASSWORD_PROPERTY} can override the default keystore password.
* <p/>
* 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).
* <p/>
* GreenMail provides the keystore resource. For customization, place your greenmail.p12 before
* greenmail JAR in the classpath.
*
* @author Wael Chatila
* @since Feb 2006
Expand All @@ -47,42 +45,45 @@ 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;
private final KeyStore ks;

// 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 {
SSLContext sslcontext = SSLContext.getInstance("TLS");
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);
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
* <p>
* Based on <a href="https://stackoverflow.com/questions/1615871">https://stackoverflow.com/questions/1615871</a>
*
* @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;
}
}

0 comments on commit 10f3ef4

Please sign in to comment.