Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,23 @@ dependencies {
| Property | Description | Default value, if any |
|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|------------------------|
| server.ssl.key-store | Path to the KeyStore, where Let's Encrypt certificates and account key are to be stored (or are already there) | |
| server.ssl.key-store | KeyStore type (i.e. PKCS12) | |
| server.ssl.key-store-pasword | Password for KeyStore with Let's Encrypt certificate and account key | |
| server.ssl.key-store-type | KeyStore type (i.e. PKCS12) | |
| server.ssl.key-store-password | Password for KeyStore with Let's Encrypt certificate and account key | |
| server.ssl.key-alias | Let's Encrypt certificate key alias in the keystore | |
| server.port | Port (secure SSL/TLS) on which your application is deployed | |
| lets-encrypt-helper.domain | Your applications' domain (i.e. example.com) | |
| lets-encrypt-helper.domain | Your application's domain (i.e. example.com) | |
| lets-encrypt-helper.contact | The contact of person responsible for the domain (i.e. mailto:[email protected]) | |
| lets-encrypt-helper.account-key-alias | Account key alias | letsencrypt-user |
| lets-encrypt-helper.letsencrypt-server | Let's Encrypt server to use | acme://letsencrypt.org |
| lets-encrypt-helper.key-size | Certificate and Account key RSA key size | 2048 |
| lets-encrypt-helper.update-before-expiry | Start trying to update certificate this time before expiration | P30D (30 days) |
| lets-encrypt-helper.busy-wait-interval | Busy wait interval for thread that checks if the certificate is valid | PT1M (1 minute) |
| lets-encrypt-helper.account-cert-validity | Validity duration for Account key | P3650D (3650 days) |
| lets-encrypt-helper.store-cert-chain | Store entire trust chain or only domain certificate (for browsers domain ceritificate is enough) | true |
| lets-encrypt-helper.store-cert-chain | Store entire trust chain or only domain certificate (for browsers domain certificate is enough) | true |
| lets-encrypt-helper.enabled | Is the helper enabled | true |
| lets-encrypt-helper.return-null-model | If challenge endpoint should return null model (i.e. `true` is sane default for cases with Thymeleaf rendering the page) | true |
| lets-encrypt-helper.development-only.http01-challenge-port | For development only, port for HTTP-01 ACME challenge | 80 |
| lets-encrypt-helper.auto-create-keystore-dir | Whether to auto-create the parent directory for the keystore if it does not exist | true |


### Example configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public class JettyWellKnownLetsEncryptChallengeEndpointConfig implements JettySe
private final boolean storeCertChain;
private final boolean enabled;
private final boolean returnNullModel;
private final boolean autoCreateKeystoreDir;
private final ServerProperties serverProperties;

// Development only properties, you can't change these for production
Expand Down Expand Up @@ -143,7 +144,8 @@ public JettyWellKnownLetsEncryptChallengeEndpointConfig(
@Value("${lets-encrypt-helper.store-cert-chain:true}") boolean storeCertChain,
@Value("${lets-encrypt-helper.enabled:true}") boolean enabled,
@Value("${lets-encrypt-helper.return-null-model:true}") boolean returnNullModel,
@Value("${lets-encrypt-helper.development-only.http01-challenge-port:80}") int http01ChallengePort
@Value("${lets-encrypt-helper.development-only.http01-challenge-port:80}") int http01ChallengePort,
@Value("${lets-encrypt-helper.auto-create-keystore-dir:true}") boolean autoCreateKeystoreDir
) {
Security.addProvider(new BouncyCastleProvider());
this.serverPort = serverPort;
Expand All @@ -160,6 +162,7 @@ public JettyWellKnownLetsEncryptChallengeEndpointConfig(
this.enabled = enabled;
this.returnNullModel = returnNullModel;
this.http01ChallengePort = http01ChallengePort;
this.autoCreateKeystoreDir = autoCreateKeystoreDir;

if (null == this.serverProperties.getSsl()) {
throw new IllegalStateException("SSL is not configured");
Expand Down Expand Up @@ -268,6 +271,8 @@ protected Instant getNow() {

private void createBasicKeystoreIfMissing() {
File keystoreFile = getKeystoreFile();
ensureParentDirExists(keystoreFile);

if (keystoreFile.exists()) {
if (!keystoreFile.canWrite()) {
throw new IllegalArgumentException(String.format("Keystore %s is not writable, certificate update is impossible", keystoreFile.getAbsolutePath()));
Expand All @@ -281,6 +286,23 @@ private void createBasicKeystoreIfMissing() {
saveKeystore(keystoreFile, keystore);
logger.info("Created basic (dummy cert, real account/domain keys) KeyStore: {}", keystoreFile.getAbsolutePath());
}

private void ensureParentDirExists(File keystoreFile) {
File parent = keystoreFile.getParentFile();
if (parent != null && !parent.exists()) {
if (autoCreateKeystoreDir) {
boolean ok = parent.mkdirs();
if (!ok && !parent.exists()) {
throw new IllegalStateException("Failed to create keystore parent directory: " + parent.getAbsolutePath());
}
logger.info("Created keystore parent directory: {}", parent.getAbsolutePath());
} else {
throw new IllegalStateException(
"Keystore parent directory does not exist: " + parent.getAbsolutePath() +
" (set lets-encrypt-helper.create-parent-dirs=true to auto-create)");
}
}
}

private TargetProtocol createObservableProtocol(SslContextFactory contextFactory) {
var observe = new TargetProtocol(contextFactory);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.github.valb3r.letsencrypthelper.jetty;

import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.web.ServerProperties;

import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class JettyKeystoreDirCreationTest {

@Test
public void createsParentDirWhenAutoCreateEnabled() throws Exception {
Path tmp = Path.of(System.getProperty("java.io.tmpdir"));
Path base = tmp.resolve("letsencrypt-helper-test-jetty").resolve("nested");
Files.deleteIfExists(base.resolve("keystore.p12"));
// ensure parent does not exist
if (Files.exists(base)) {
Files.walk(base).map(Path::toFile).forEach(File::delete);
}

String keystorePath = "file:" + base.resolve("keystore.p12").toAbsolutePath().toString();

ServerProperties props = new ServerProperties();
// Find setSsl method to determine the SSL parameter type dynamically
java.lang.reflect.Method setSsl = null;
for (var m : ServerProperties.class.getMethods()) {
if (m.getName().equals("setSsl") && m.getParameterCount() == 1) {
setSsl = m;
break;
}
}
if (setSsl == null) throw new IllegalStateException("ServerProperties.setSsl method not found");
Class<?> sslClass = setSsl.getParameterTypes()[0];
Object ssl = sslClass.getDeclaredConstructor().newInstance();
var setKeyStore = sslClass.getMethod("setKeyStore", String.class);
var setKeyStorePassword = sslClass.getMethod("setKeyStorePassword", String.class);
var setKeyAlias = sslClass.getMethod("setKeyAlias", String.class);
var setKeyStoreType = sslClass.getMethod("setKeyStoreType", String.class);
setKeyStore.invoke(ssl, keystorePath);
setKeyStorePassword.invoke(ssl, "changeit");
setKeyAlias.invoke(ssl, "alias");
setKeyStoreType.invoke(ssl, "PKCS12");
setSsl.invoke(props, ssl);

var cfg = new JettyWellKnownLetsEncryptChallengeEndpointConfig(
props, 443, "example.com", "mailto:[email protected]",
"acct", "acme://letsencrypt.org", 1024,
Duration.ofDays(30), Duration.ofMinutes(1), Duration.ofDays(3650), true, true, true, 80, true
);

Method m = JettyWellKnownLetsEncryptChallengeEndpointConfig.class.getDeclaredMethod("createBasicKeystoreIfMissing");
m.setAccessible(true);

try {
m.invoke(cfg);

assertThat(Files.exists(base)).isTrue();
assertThat(Files.exists(base.resolve("keystore.p12"))).isTrue();
} finally {
// cleanup
Files.deleteIfExists(base.resolve("keystore.p12"));
if (Files.exists(base)) {
Files.deleteIfExists(base);
Path parent = base.getParent();
if (parent != null && Files.exists(parent)) {
Files.deleteIfExists(parent);
}
}
}
}

@Test
public void failsWhenAutoCreateDisabled() throws Exception {
Path tmp = Path.of(System.getProperty("java.io.tmpdir"));
Path base = tmp.resolve("letsencrypt-helper-test-jetty").resolve("nested-no-create");
Files.deleteIfExists(base.resolve("keystore.p12"));
if (Files.exists(base)) {
Files.walk(base).map(Path::toFile).forEach(File::delete);
}

String keystorePath = "file:" + base.resolve("keystore.p12").toAbsolutePath().toString();

ServerProperties props = new ServerProperties();
java.lang.reflect.Method setSsl2 = null;
for (var m : ServerProperties.class.getMethods()) {
if (m.getName().equals("setSsl") && m.getParameterCount() == 1) {
setSsl2 = m;
break;
}
}
if (setSsl2 == null) throw new IllegalStateException("ServerProperties.setSsl method not found");
Class<?> sslClass2 = setSsl2.getParameterTypes()[0];
Object ssl2 = sslClass2.getDeclaredConstructor().newInstance();
var setKeyStore2 = sslClass2.getMethod("setKeyStore", String.class);
var setKeyStorePassword2 = sslClass2.getMethod("setKeyStorePassword", String.class);
var setKeyAlias2 = sslClass2.getMethod("setKeyAlias", String.class);
var setKeyStoreType2 = sslClass2.getMethod("setKeyStoreType", String.class);
setKeyStore2.invoke(ssl2, keystorePath);
setKeyStorePassword2.invoke(ssl2, "changeit");
setKeyAlias2.invoke(ssl2, "alias");
setKeyStoreType2.invoke(ssl2, "PKCS12");
setSsl2.invoke(props, ssl2);

var cfg = new JettyWellKnownLetsEncryptChallengeEndpointConfig(
props, 443, "example.com", "mailto:[email protected]",
"acct", "acme://letsencrypt.org", 1024,
Duration.ofDays(30), Duration.ofMinutes(1), Duration.ofDays(3650), true, true, true, 80, false
);

Method m = JettyWellKnownLetsEncryptChallengeEndpointConfig.class.getDeclaredMethod("createBasicKeystoreIfMissing");
m.setAccessible(true);

assertThatThrownBy(() -> m.invoke(cfg))
.satisfies(t -> {
assertThat(t.getCause()).isInstanceOf(IllegalStateException.class);
assertThat(t.getCause().getMessage()).contains("Keystore parent directory does not exist");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public class TomcatWellKnownLetsEncryptChallengeEndpointConfig implements Tomcat
private final boolean storeCertChain;
private final boolean enabled;
private final boolean returnNullModel;
private final boolean autoCreateKeystoreDir;
private final ServerProperties serverProperties;

// Development only properties, you can't change these for production
Expand Down Expand Up @@ -142,7 +143,8 @@ public TomcatWellKnownLetsEncryptChallengeEndpointConfig(
@Value("${lets-encrypt-helper.store-cert-chain:true}") boolean storeCertChain,
@Value("${lets-encrypt-helper.enabled:true}") boolean enabled,
@Value("${lets-encrypt-helper.return-null-model:true}") boolean returnNullModel,
@Value("${lets-encrypt-helper.development-only.http01-challenge-port:80}") int http01ChallengePort
@Value("${lets-encrypt-helper.development-only.http01-challenge-port:80}") int http01ChallengePort,
@Value("${lets-encrypt-helper.create-parent-dirs:true}") boolean autoCreateKeystoreDir
) {
Security.addProvider(new BouncyCastleProvider());
this.serverPort = serverPort;
Expand All @@ -159,6 +161,7 @@ public TomcatWellKnownLetsEncryptChallengeEndpointConfig(
this.enabled = enabled;
this.returnNullModel = returnNullModel;
this.http01ChallengePort = http01ChallengePort;
this.autoCreateKeystoreDir = autoCreateKeystoreDir;

if (null == this.serverProperties.getSsl()) {
throw new IllegalStateException("SSL is not configured");
Expand Down Expand Up @@ -289,6 +292,8 @@ protected SSLHostConfigCertificate findMatchingCertificate(SSLHostConfig config,

private void createBasicKeystoreIfMissing() {
File keystoreFile = getKeystoreFile();
ensureParentDirExists(keystoreFile);

if (keystoreFile.exists()) {
if (!keystoreFile.canWrite()) {
throw new IllegalArgumentException(String.format("Keystore %s is not writable, certificate update is impossible", keystoreFile.getAbsolutePath()));
Expand All @@ -302,6 +307,23 @@ private void createBasicKeystoreIfMissing() {
saveKeystore(keystoreFile, keystore);
logger.info("Created basic (dummy cert, real account/domain keys) KeyStore: {}", keystoreFile.getAbsolutePath());
}

private void ensureParentDirExists(File keystoreFile) {
File parent = keystoreFile.getParentFile();
if (parent != null && !parent.exists()) {
if (autoCreateKeystoreDir) {
boolean ok = parent.mkdirs();
if (!ok && !parent.exists()) {
throw new IllegalStateException("Failed to create keystore parent directory: " + parent.getAbsolutePath());
}
logger.info("Created keystore parent directory: {}", parent.getAbsolutePath());
} else {
throw new IllegalStateException(
"Keystore parent directory does not exist: " + parent.getAbsolutePath() +
" (set lets-encrypt-helper.create-parent-dirs=true to auto-create)");
}
}
}

private TargetProtocol createObservableProtocol(AbstractHttp11Protocol<?> protocol) {
var observe = new TargetProtocol(sslHostConfig, protocol);
Expand Down
Loading
Loading