diff --git a/src/zutil/net/acme/AcmeClient.java b/src/zutil/net/acme/AcmeClient.java index 27d8d0c..8fba45f 100644 --- a/src/zutil/net/acme/AcmeClient.java +++ b/src/zutil/net/acme/AcmeClient.java @@ -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; + } + } } diff --git a/src/zutil/net/acme/AcmeDataStore.java b/src/zutil/net/acme/AcmeDataStore.java index bea8310..db1cb49 100644 --- a/src/zutil/net/acme/AcmeDataStore.java +++ b/src/zutil/net/acme/AcmeDataStore.java @@ -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); - } \ No newline at end of file + /** + * 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); +} \ No newline at end of file diff --git a/src/zutil/net/acme/AcmeFileDataStore.java b/src/zutil/net/acme/AcmeFileDataStore.java index b2e7ac4..77fde8b 100644 --- a/src/zutil/net/acme/AcmeFileDataStore.java +++ b/src/zutil/net/acme/AcmeFileDataStore.java @@ -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(); + } + } } diff --git a/src/zutil/net/http/HttpServer.java b/src/zutil/net/http/HttpServer.java index 49a4e9b..39e9ea2 100755 --- a/src/zutil/net/http/HttpServer.java +++ b/src/zutil/net/http/HttpServer.java @@ -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 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; diff --git a/src/zutil/net/threaded/ThreadedTCPNetworkServer.java b/src/zutil/net/threaded/ThreadedTCPNetworkServer.java index 16b45a0..5dbbd94 100755 --- a/src/zutil/net/threaded/ThreadedTCPNetworkServer.java +++ b/src/zutil/net/threaded/ThreadedTCPNetworkServer.java @@ -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. */