ACME client will now reuse existing certificate instead of requesting new one every time.

This commit is contained in:
Ziver Koc 2021-08-22 01:41:48 +02:00
parent 3552c21404
commit 3afb1e241e
5 changed files with 227 additions and 146 deletions

View file

@ -3,6 +3,7 @@ package zutil.net.acme;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.Security; import java.security.Security;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -114,60 +115,68 @@ public class AcmeClient {
if (order == null) if (order == null)
throw new IllegalStateException("prepareRequest() method has not been called before the request of certificate."); throw new IllegalStateException("prepareRequest() method has not been called before the request of certificate.");
// Perform all required domain authorizations X509Certificate certificate = dataStore.getCertificate();
for (Challenge challenge : challenges) {
execDomainChallenge(challenge);
}
// Load or create a key pair for the domains. This should not be same as the userKeyPair! // TODO: Check if certificate is valid for the given domains
KeyPair domainKeyPair = dataStore.getDomainKeyPair(); if (!isCertificateValid(certificate)) {
// Perform all required domain authorizations
if (domainKeyPair == null) { for (Challenge challenge : challenges) {
logger.fine("Creating new domain keys."); execDomainChallenge(challenge);
domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
dataStore.storeDomainKeyPair(domainKeyPair);
}
// Generate one "Certificate Signing Request" for all the domains, and sign it with the domain key pair.
CSRBuilder csrBuilder = new CSRBuilder();
csrBuilder.addDomains(domains);
csrBuilder.sign(domainKeyPair);
order.execute(csrBuilder.getEncoded()); // Order the certificate
// Wait for the order to complete
try {
for (int attempts = 0; attempts < 10; attempts++) {
// Did the order pass or fail?
if (order.getStatus() == Status.VALID) {
break;
} else if (order.getStatus() == Status.INVALID) {
throw new AcmeException("Certificate order has failed, reason: " + order.getError());
}
// Wait for a few seconds
long sleep = 100L + 1000L * attempts;
logger.fine("Challenge not yet completed, sleeping for: " + StringUtil.formatTimeToString(sleep));
Thread.sleep(sleep);
// Then update the status
order.update();
} }
} catch (InterruptedException ex) {
logger.log(Level.SEVERE, "Interrupted", ex); // Load or create a key pair for the domains. This should not be same as the userKeyPair!
KeyPair domainKeyPair = dataStore.getDomainKeyPair();
if (domainKeyPair == null) {
logger.fine("Creating new domain keys.");
domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
dataStore.storeDomainKeyPair(domainKeyPair);
}
// Generate one "Certificate Signing Request" for all the domains, and sign it with the domain key pair.
CSRBuilder csrBuilder = new CSRBuilder();
csrBuilder.addDomains(domains);
csrBuilder.sign(domainKeyPair);
order.execute(csrBuilder.getEncoded()); // Order the certificate
// Wait for the order to complete
try {
for (int attempts = 0; attempts < 10; attempts++) {
// Did the order pass or fail?
if (order.getStatus() == Status.VALID) {
break;
} else if (order.getStatus() == Status.INVALID) {
throw new AcmeException("Certificate order has failed, reason: " + order.getError());
}
// Wait for a few seconds
long sleep = 100L + 1000L * attempts;
logger.fine("Challenge not yet completed, sleeping for: " + StringUtil.formatTimeToString(sleep));
Thread.sleep(sleep);
// Then update the status
order.update();
}
} catch (InterruptedException ex) {
logger.log(Level.SEVERE, "Interrupted", ex);
}
// Get the certificate
certificate = order.getCertificate().getCertificate();
dataStore.storeCertificate(certificate);
logger.info("Successfully created new certificate for domains: " + StringUtil.join(",", domains));
} else {
logger.info("Using existing certificate for domains: " + StringUtil.join(",", domains));
} }
// Get the certificate
Certificate certificate = order.getCertificate();
logger.info("The certificate for domains '" + StringUtil.join(",", domains) + "' has been successfully generated.");
// Cleanup // Cleanup
order = null; order = null;
challenges.clear(); challenges.clear();
return certificate.getCertificate(); return certificate;
} }
@ -262,4 +271,23 @@ public class AcmeClient {
logger.fine("Domain challenge executed successfully."); logger.fine("Domain challenge executed successfully.");
} }
/**
* A helper method to check if a certificate is still valid.
*
* @param certificate the certificate to validate.
* @return true if the certificate is still valid otherwise false
*/
public static boolean isCertificateValid(X509Certificate certificate) {
if (certificate == null)
return false;
try {
certificate.checkValidity();
return true;
} catch (GeneralSecurityException e) {
return false;
}
}
} }

View file

@ -2,42 +2,59 @@ package zutil.net.acme;
import java.net.URL; import java.net.URL;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.cert.X509Certificate;
public interface AcmeDataStore { public interface AcmeDataStore {
/** /**
* Loads an accounts key pair. * Read in an account location URL.
* *
* @return a KeyPair for the account, null if no KeyPair is found. * @return a URL to the account, this is used as an identifier.
*/ */
URL getAccountLocation(); URL getAccountLocation();
/** /**
* Retrieve an account key pair. * Retrieve an account key pair.
* *
* @return a KeyPair object for the account, null if no KeyPair is found. * @return a KeyPair object for the account, null if no KeyPair is found.
*/ */
KeyPair getAccountKeyPair(); KeyPair getAccountKeyPair();
/** /**
* Stores an accounts key pair for later usage. * Store an accounts key pair for later usage.
* *
* @param accountLocation the URL to the account profile, this is used as an identifier to the ACME service. * @param accountLocation the URL to the account profile, this is used as an identifier to the ACME service.
* @param accountKeyPair the keys for the user account * @param accountKeyPair the keys for the user account
*/ */
void storeAccountKeyPair(URL accountLocation, KeyPair accountKeyPair); void storeAccountKeyPair(URL accountLocation, KeyPair accountKeyPair);
/**
* Loads a domain key pair.
*
* @return a KeyPair object for the domains, null if no KeyPar was found.
*/
KeyPair getDomainKeyPair();
/** /**
* Stores a domain key pair for later usage. * Read in a domain key pair.
* *
* @param keyPair the keys for the domain * @return a KeyPair object for the domains, null if no KeyPar was found.
*/ */
void storeDomainKeyPair(KeyPair keyPair); KeyPair getDomainKeyPair();
}
/**
* Store a domain key pair for later usage.
*
* @param keyPair the keys for the domain
*/
void storeDomainKeyPair(KeyPair keyPair);
/**
* Loads a certificate
*
* @return a certificate that has previously been generated, null if no certificate is available.
*/
X509Certificate getCertificate();
/**
* Store a domain key pair for later usage.
*
* @param certificate the certificate to be stored
*/
void storeCertificate(X509Certificate certificate);
}

View file

@ -1,17 +1,23 @@
package zutil.net.acme; package zutil.net.acme;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.util.KeyPairUtils; import org.shredzone.acme4j.util.KeyPairUtils;
import zutil.io.file.FileUtil; import zutil.io.file.FileUtil;
import java.io.*; import java.io.*;
import java.net.URL; import java.net.URL;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class AcmeFileDataStore implements AcmeDataStore { public class AcmeFileDataStore implements AcmeDataStore {
private final File accountLocationFile; private final File accountLocationFile;
private final File accountKeyFile; private final File accountKeyFile;
private final File domainKeyFile; private final File domainKeyFile;
private final File certificateFile;
/** /**
@ -23,6 +29,7 @@ public class AcmeFileDataStore implements AcmeDataStore {
this.accountLocationFile = new File(folder, "accountLocation.cfg"); this.accountLocationFile = new File(folder, "accountLocation.cfg");
this.accountKeyFile = new File(folder, "account.key"); this.accountKeyFile = new File(folder, "account.key");
this.domainKeyFile = new File(folder, "domain.key"); this.domainKeyFile = new File(folder, "domain.key");
this.certificateFile = new File(folder, "certificate.pem");
} }
@ -77,4 +84,27 @@ public class AcmeFileDataStore implements AcmeDataStore {
e.printStackTrace(); e.printStackTrace();
} }
} }
@Override
public X509Certificate getCertificate() {
if (certificateFile.exists()) {
try (FileInputStream in = new FileInputStream(certificateFile)) {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(in);
} catch (IOException | CertificateException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public void storeCertificate(X509Certificate certificate) {
try(FileWriter out = new FileWriter(certificateFile)) {
AcmeUtils.writeToPem(certificate.getEncoded(), AcmeUtils.PemLabel.CERTIFICATE, out);
} catch (IOException | CertificateEncodingException e) {
e.printStackTrace();
}
}
} }

View file

@ -34,8 +34,9 @@ import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.Socket; import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -60,7 +61,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
public static final String SESSION_KEY_ID = "session_id"; public static final String SESSION_KEY_ID = "session_id";
public static final String SESSION_KEY_TTL = "session_ttl"; public static final String SESSION_KEY_TTL = "session_ttl";
public static final String SERVER_NAME = "Zutil HttpServer"; public static final String SERVER_NAME = "Zutil HttpServer";
public static final int SESSION_TTL = 10*60*1000; // in milliseconds public static final int SESSION_TTL = 10*60*1000; // in milliseconds
private Map<String,HttpPage> pages = new ConcurrentHashMap<>();; private Map<String,HttpPage> pages = new ConcurrentHashMap<>();;
@ -68,6 +69,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
private int nextSessionId = 0; private int nextSessionId = 0;
private HttpPage defaultPage = null; private HttpPage defaultPage = null;
/** /**
* Creates a new instance of the sever * Creates a new instance of the sever
* *
@ -75,9 +77,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
*/ */
public HttpServer(int port) throws IOException { public HttpServer(int port) throws IOException {
super(port); super(port);
initGarbageCollector(); initialize("HTTP");
logger.info("HTTP Server ready and listening to port: " + port);
} }
/** /**
* Creates a new instance of the sever which accepts SSL connections * Creates a new instance of the sever which accepts SSL connections
@ -85,32 +85,43 @@ public class HttpServer extends ThreadedTCPNetworkServer{
* @param port The port that the server should listen to * @param port The port that the server should listen to
* @param certificate The certificate that should be used for the servers SSL connections * @param certificate The certificate that should be used for the servers SSL connections
*/ */
public HttpServer(int port, Certificate certificate) throws IOException { public HttpServer(int port, Certificate certificate) throws IOException, GeneralSecurityException {
super(port, certificate); super(port, certificate);
initGarbageCollector(); initialize("HTTPS");
logger.info("HTTPS Server ready and listening to port: " + port);
} }
/** /**
* Creates a new instance of the sever which accepts SSL connections * Creates a new instance of the sever which accepts SSL connections
* *
* @param port The port that the server should listen to * @param port The port that the server should listen to
* @param keyStore The keyStore containing the certificate to use for the servers SSL connections * @param keyStoreFile The keyStore file containing the certificate to use for the servers SSL connections
* @param keyStorePass The password to unlock the key store. * @param keyStorePass The password to unlock the key store.
*/ */
public HttpServer(int port, File keyStore, char[] keyStorePass) throws IOException { public HttpServer(int port, File keyStoreFile, char[] keyStorePass) throws IOException, GeneralSecurityException {
super(port, keyStore, keyStorePass); super(port, keyStoreFile, keyStorePass);
initGarbageCollector(); initialize("HTTPS");
logger.info("HTTPS Server ready and listening to port: " + port);
}
private void initGarbageCollector() {
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
exec.scheduleWithFixedDelay(new SessionGarbageCollector(), 10000, SESSION_TTL / 2, TimeUnit.MILLISECONDS);
} }
/** /**
* This class acts as an garbage collector that * Creates a new instance of the sever which accepts SSL connections
*
* @param port The port that the server should listen to
* @param keyStore The keyStore object containing the certificate to use for the servers SSL connections
* @param keyStorePass The password to unlock the key store.
*/
public HttpServer(int port, KeyStore keyStore, char[] keyStorePass) throws IOException, GeneralSecurityException {
super(port, keyStore, keyStorePass);
initialize("HTTPS");
}
private void initialize(String httpType) {
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
exec.scheduleWithFixedDelay(new SessionGarbageCollector(), 10000, SESSION_TTL / 2, TimeUnit.MILLISECONDS);
logger.info(httpType + " Server ready and listening to port: " + httpType.toLowerCase() + "://localhost:" + getPort());
}
/**
* This class acts as a garbage collector that
* removes old sessions from the session HashMap * removes old sessions from the session HashMap
*/ */
private class SessionGarbageCollector implements Runnable { private class SessionGarbageCollector implements Runnable {
@ -186,7 +197,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
/** /**
* Internal class that handles all the requests * Internal class that handles all the requests
*/ */
protected class HttpServerThread implements ThreadedTCPNetworkServerThread{ protected class HttpServerThread implements ThreadedTCPNetworkServerThread {
private HttpPrintStream out; private HttpPrintStream out;
private BufferedInputStream in; private BufferedInputStream in;
private Socket socket; private Socket socket;

View file

@ -26,8 +26,8 @@ package zutil.net.threaded;
import zutil.log.LogUtil; import zutil.log.LogUtil;
import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLServerSocketFactory;
import java.io.File; import java.io.File;
@ -37,7 +37,6 @@ import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.security.*; import java.security.*;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
@ -63,7 +62,16 @@ public abstract class ThreadedTCPNetworkServer extends Thread {
*/ */
public ThreadedTCPNetworkServer(int port) throws IOException { public ThreadedTCPNetworkServer(int port) throws IOException {
this.port = port; this.port = port;
this.serverSocket = new ServerSocket(port); this.serverSocket = ServerSocketFactory.getDefault().createServerSocket(port);
}
/**
* Creates a new SSL instance of the sever.
*
* @param port the port that the server should listen to.
* @param certificate the certificate for the server domain.
*/
public ThreadedTCPNetworkServer(int port, Certificate certificate) throws IOException, GeneralSecurityException {
this(port, getKeyStore(certificate), null);
} }
/** /**
* Creates a new SSL instance of the sever. * Creates a new SSL instance of the sever.
@ -72,79 +80,66 @@ public abstract class ThreadedTCPNetworkServer extends Thread {
* @param keyStoreFile the path to the key store file containing the server certificates * @param keyStoreFile the path to the key store file containing the server certificates
* @param keyStorePass the password to decrypt the key store file. * @param keyStorePass the password to decrypt the key store file.
*/ */
public ThreadedTCPNetworkServer(int port, File keyStoreFile, char[] keyStorePass) throws IOException { public ThreadedTCPNetworkServer(int port, File keyStoreFile, char[] keyStorePass) throws IOException, GeneralSecurityException {
this.port = port; this(port, getKeyStore(keyStoreFile, keyStorePass), keyStorePass);
this.serverSocket = createSSLSocket(port, keyStoreFile, keyStorePass);
} }
/** /**
* Creates a new SSL instance of the sever. * Creates a new SSL instance of the sever.
* *
* @param port the port that the server should listen to. * @param port the port that the server should listen to.
* @param certificate the certificate for the servers domain. * @param keyStore the KeyStore that contains the certificate to be used by the server
* @param keyStorePass the password to decrypt the key store file, null if there is no password set
*/ */
public ThreadedTCPNetworkServer(int port, Certificate certificate) throws IOException { public ThreadedTCPNetworkServer(int port, KeyStore keyStore, char[] keyStorePass) throws IOException, GeneralSecurityException {
this.port = port; this.port = port;
this.serverSocket = createSSLSocket(port, certificate); this.serverSocket = getSSLServerSocketFactory(keyStore, keyStorePass).createServerSocket(port);
} }
/** /**
* Initiates a SSLServerSocket * Initiates a SSLServerSocket
* *
* @param port the port the server should to * @param certificate the certificate for the server domain.
* @return a SSLServerSocket object
*/
private static KeyStore getKeyStore(Certificate certificate) throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null); // Create empty keystore
keyStore.setCertificateEntry("ssl_server_cert", certificate);
return keyStore;
}
/**
* Initiates a SSLServerSocket
*
* @param keyStoreFile the cert file location * @param keyStoreFile the cert file location
* @param keyStorePass the password for the cert file * @param keyStorePass the password for the cert file, null if there is no password set
* @return a SSLServerSocket object * @return a SSLServerSocket object
*/ */
private static ServerSocket createSSLSocket(int port, File keyStoreFile, char[] keyStorePass) throws IOException{ private static KeyStore getKeyStore(File keyStoreFile, char[] keyStorePass) throws IOException, GeneralSecurityException {
try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(new FileInputStream(keyStoreFile), keyStorePass);
keyStore.load(new FileInputStream(keyStoreFile), keyStorePass);
return createSSLSocket(port, keyStore, keyStorePass); return keyStore;
} catch (GeneralSecurityException e) {
throw new IOException("Unable to configure certificate.", e);
}
} }
/** /**
* Initiates a SSLServerSocket * Creates a new SSLServerSocketFactory
* *
* @param port the port the server should to.
* @param certificate the certificate for the servers domain.
* @return a SSLServerSocket object
*/
private static ServerSocket createSSLSocket(int port, Certificate certificate) throws IOException{
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null); // Create empty keystore
keyStore.setCertificateEntry("main_certificate", certificate);
return createSSLSocket(port, keyStore, null);
} catch (GeneralSecurityException e) {
throw new IOException("Unable to configure certificate.", e);
}
}
/**
* Initiates a SSLServerSocket
*
* @param port the port the server should to.
* @param keyStore the key store containing the domain certificates * @param keyStore the key store containing the domain certificates
* @param keyStorePass the password for the cert file * @param keyStorePass the password for the cert file, null if there is no password set
* @return a SSLServerSocket object * @return a SSLServerSocketFactory object
*/ */
private static ServerSocket createSSLSocket(int port, KeyStore keyStore, char[] keyStorePass) private static SSLServerSocketFactory getSSLServerSocketFactory(KeyStore keyStore, char[] keyStorePass)
throws IOException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, KeyManagementException { throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, KeyManagementException {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePass); keyManagerFactory.init(keyStore, keyStorePass);
SSLContext ctx = SSLContext.getInstance("TLS"); SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(keyManagerFactory.getKeyManagers(), null, SecureRandom.getInstanceStrong()); ctx.init(keyManagerFactory.getKeyManagers(), null, SecureRandom.getInstanceStrong());
SSLServerSocketFactory socketFactory = ctx.getServerSocketFactory(); return ctx.getServerSocketFactory();
return socketFactory.createServerSocket(port);
} }
/** /**
* @return the port that this TCP server is listening to. * @return the port that this TCP server is listening to.
*/ */