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.net.URI;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
@ -114,60 +115,68 @@ public class AcmeClient {
if (order == null)
throw new IllegalStateException("prepareRequest() method has not been called before the request of certificate.");
// Perform all required domain authorizations
for (Challenge challenge : challenges) {
execDomainChallenge(challenge);
}
X509Certificate certificate = dataStore.getCertificate();
// 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();
// TODO: Check if certificate is valid for the given domains
if (!isCertificateValid(certificate)) {
// Perform all required domain authorizations
for (Challenge challenge : challenges) {
execDomainChallenge(challenge);
}
} 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
order = null;
challenges.clear();
return certificate.getCertificate();
return certificate;
}
@ -262,4 +271,23 @@ public class AcmeClient {
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.security.KeyPair;
import java.security.cert.X509Certificate;
public interface AcmeDataStore {
/**
* Loads an accounts key pair.
*
* @return a KeyPair for the account, null if no KeyPair is found.
*/
URL getAccountLocation();
/**
* Read in an account location URL.
*
* @return a URL to the account, this is used as an identifier.
*/
URL getAccountLocation();
/**
* Retrieve an account key pair.
*
* @return a KeyPair object for the account, null if no KeyPair is found.
*/
KeyPair getAccountKeyPair();
/**
* Retrieve an account key pair.
*
* @return a KeyPair object for the account, null if no KeyPair is found.
*/
KeyPair getAccountKeyPair();
/**
* Stores 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 accountKeyPair the keys for the user account
*/
void storeAccountKeyPair(URL accountLocation, KeyPair accountKeyPair);
/**
* 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 accountKeyPair the keys for the user account
*/
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.
*
* @param keyPair the keys for the domain
*/
void storeDomainKeyPair(KeyPair keyPair);
}
/**
* Read in a domain key pair.
*
* @return a KeyPair object for the domains, null if no KeyPar was found.
*/
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;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.util.KeyPairUtils;
import zutil.io.file.FileUtil;
import java.io.*;
import java.net.URL;
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 {
private final File accountLocationFile;
private final File accountKeyFile;
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.accountKeyFile = new File(folder, "account.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();
}
}
@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.IOException;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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_TTL = "session_ttl";
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<>();;
@ -68,6 +69,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
private int nextSessionId = 0;
private HttpPage defaultPage = null;
/**
* Creates a new instance of the sever
*
@ -75,9 +77,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
*/
public HttpServer(int port) throws IOException {
super(port);
initGarbageCollector();
logger.info("HTTP Server ready and listening to port: " + port);
initialize("HTTP");
}
/**
* 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 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);
initGarbageCollector();
logger.info("HTTPS Server ready and listening to port: " + port);
initialize("HTTPS");
}
/**
* 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 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.
*/
public HttpServer(int port, File keyStore, char[] keyStorePass) throws IOException {
super(port, keyStore, keyStorePass);
initGarbageCollector();
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);
public HttpServer(int port, File keyStoreFile, char[] keyStorePass) throws IOException, GeneralSecurityException {
super(port, keyStoreFile, keyStorePass);
initialize("HTTPS");
}
/**
* 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
*/
private class SessionGarbageCollector implements Runnable {
@ -186,7 +197,7 @@ public class HttpServer extends ThreadedTCPNetworkServer{
/**
* Internal class that handles all the requests
*/
protected class HttpServerThread implements ThreadedTCPNetworkServerThread{
protected class HttpServerThread implements ThreadedTCPNetworkServerThread {
private HttpPrintStream out;
private BufferedInputStream in;
private Socket socket;

View file

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