From 2aa4989f4003e0838dd848744c2b9ff71356fe6e Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 23 Oct 2024 11:08:54 -0400 Subject: [PATCH] feat(tls): add configuration options for supplying bare TLS server key (#507) --- README.md | 8 ++ .../java/io/cryostat/agent/ConfigModule.java | 74 ++++++++++++++++++ .../java/io/cryostat/agent/MainModule.java | 76 +++++++++++++++++-- .../META-INF/microprofile-config.properties | 25 ++++-- 4 files changed, 172 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index af91bcd4..9a044f8e 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,14 @@ and how it advertises itself to a Cryostat server instance. Properties that requ - [ ] `cryostat.agent.webserver.tls.keystore.pass.charset` [`String`]: the character set used by the HTTPS server keystore's password. Default `utf-8`. - [ ] `cryostat.agent.webserver.tls.keystore.file` [`String`]: the filepath to the HTTPS server keystore - [ ] `cryostat.agent.webserver.tls.keystore.type` [`String`]: the type of keystore used for the Agent's HTTPS server. Default `PKCS12`. +- [ ] `cryostat.agent.webserver.tls.key.alias` [`String`]: the alias used for the keystore entry to contain this key for the HTTPS server. +- [ ] `cryostat.agent.webserver.tls.key.path` [`String`]: the filepath to the TLS key used by the HTTPS server. +- [ ] `cryostat.agent.webserver.tls.key.charset` [`String`]: the string encoding of the TLS key file. Default `utf-8`. +- [ ] `cryostat.agent.webserver.tls.key.encoding` [`String`]: the certificate encoding of the TLS key file. Default `PKCS1`. +- [ ] `cryostat.agent.webserver.tls.key.type` [`String`]: the key type of the TLS key. Default `RSA`. +- [ ] `cryostat.agent.webserver.tls.key.pass.file` [`String`]: the path to a file containing the password to unlock the TLS key. +- [ ] `cryostat.agent.webserver.tls.key.pass-charset` [`String`]: the string encoding of the file containing the TLS key password. Default `utf-8`. +- [ ] `cryostat.agent.webserver.tls.key.pass` [`String`]: the TLS key password value. Providing the password as a mounted file is preferred. - [ ] `cryostat.agent.webserver.tls.cert.alias` [`String`]: the alias for the certificate stored in the HTTPS server keystore. Default `serverCert`. - [ ] `cryostat.agent.webserver.tls.cert.file` [`String`]: the filepath to the certificate to be stored by the HTTPS server keystore - [ ] `cryostat.agent.webserver.tls.cert.type` [`String`]: the type of certificate that the HTTPS server keystore will present. Default `X.509`. diff --git a/src/main/java/io/cryostat/agent/ConfigModule.java b/src/main/java/io/cryostat/agent/ConfigModule.java index 1f245cbc..e2bb4675 100644 --- a/src/main/java/io/cryostat/agent/ConfigModule.java +++ b/src/main/java/io/cryostat/agent/ConfigModule.java @@ -140,6 +140,24 @@ public abstract class ConfigModule { "cryostat.agent.webserver.tls.keystore.file"; public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEYSTORE_TYPE = "cryostat.agent.webserver.tls.keystore.type"; + + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ALIAS = + "cryostat.agent.webserver.tls.key.alias"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PATH = + "cryostat.agent.webserver.tls.key.path"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_CHARSET = + "cryostat.agent.webserver.tls.key.charset"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ENCODING = + "cryostat.agent.webserver.tls.key.encoding"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_TYPE = + "cryostat.agent.webserver.tls.key.type"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_FILE = + "cryostat.agent.webserver.tls.key.pass.file"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_CHARSET = + "cryostat.agent.webserver.tls.key.pass-charset"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS = + "cryostat.agent.webserver.tls.key.pass"; + public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_CERT_ALIAS = "cryostat.agent.webserver.tls.cert.alias"; public static final String CRYOSTAT_AGENT_WEBSERVER_TLS_CERT_FILE = @@ -608,6 +626,62 @@ public static String provideCryostatAgentWebserverTlsKeyStoreType(Config config) return config.getValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEYSTORE_TYPE, String.class); } + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ALIAS) + public static String provideCryostatAgentWebserverTlsKeyAlias(Config config) { + return config.getValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ALIAS, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PATH) + public static Optional provideCryostatAgentWebserverTlsKeyPath(Config config) { + return config.getOptionalValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PATH, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_CHARSET) + public static String provideCryostatAgentWebserverTlsKeyCharset(Config config) { + return config.getValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_CHARSET, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ENCODING) + public static String provideCryostatAgentWebserverTlsKeyEncoding(Config config) { + return config.getValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ENCODING, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_TYPE) + public static String provideCryostatAgentWebserverTlsKeyType(Config config) { + return config.getValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_TYPE, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_FILE) + public static Optional provideCryostatAgentWebserverTlsKeyPassFile(Config config) { + return config.getOptionalValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_FILE, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_CHARSET) + public static String provideCryostatAgentWebserverTlsKeyPassCharset(Config config) { + return config.getValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_CHARSET, String.class); + } + + @Provides + @Singleton + @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS) + public static Optional provideCryostatAgentWebserverTlsKeyPass(Config config) { + return config.getOptionalValue(CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS, String.class); + } + @Provides @Singleton @Named(CRYOSTAT_AGENT_WEBSERVER_TLS_CERT_ALIAS) diff --git a/src/main/java/io/cryostat/agent/MainModule.java b/src/main/java/io/cryostat/agent/MainModule.java index 605fbfc7..6033b8e0 100644 --- a/src/main/java/io/cryostat/agent/MainModule.java +++ b/src/main/java/io/cryostat/agent/MainModule.java @@ -459,31 +459,46 @@ public static Optional provideServerSslContext( String passFileCharset, @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEYSTORE_FILE) Optional keyStoreFilePath, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ALIAS) String keyAlias, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PATH) Optional keyFilePath, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS) Optional keyPass, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_FILE) + Optional keyPassFile, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PASS_CHARSET) + String keyPassCharset, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_PATH) Optional keyPath, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_ENCODING) String keyEncoding, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_CHARSET) String keyCharset, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEY_TYPE) String keyType, @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_KEYSTORE_TYPE) String keyStoreType, @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_CERT_ALIAS) String certAlias, @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_CERT_FILE) Optional certFilePath, @Named(ConfigModule.CRYOSTAT_AGENT_WEBSERVER_TLS_CERT_TYPE) String certType) { boolean ssl = - keyStorePassFile.isPresent() - && keyStoreFilePath.isPresent() + (keyStoreFilePath.isPresent() || keyFilePath.isPresent()) + && keyStorePassFile.isPresent() && certFilePath.isPresent(); if (!ssl) { if (keyStorePassFile.isPresent() + || keyFilePath.isPresent() || keyStoreFilePath.isPresent() || certFilePath.isPresent()) { throw new IllegalArgumentException( - "The file paths for the keystore, keystore password, and certificate must" - + " ALL be provided to set up HTTPS connections. Otherwise, make sure" - + " they are all unset to use an HTTP server."); + "The file paths for the keystore or key file, keystore password, and" + + " certificate must ALL be provided to set up HTTPS connections." + + " Otherwise, make sure they are all unset to use an HTTP server."); } return Optional.empty(); } + InputStream keystore = null; try (InputStream pass = new FileInputStream(keyStorePassFile.get()); - InputStream keystore = new FileInputStream(keyStoreFilePath.get()); InputStream certFile = new FileInputStream(certFilePath.get())) { SSLContext sslContext = SSLContext.getInstance(serverTlsVersion); + if (keyStoreFilePath.isPresent()) { + keystore = new FileInputStream(keyStoreFilePath.get()); + } // initialize keystore String password = IOUtils.toString(pass, Charset.forName(passFileCharset)); @@ -503,6 +518,46 @@ public static Optional provideServerSslContext( } ks.setCertificateEntry(certAlias, cert); + if (keyFilePath.isPresent()) { + Optional kp = readPass(keyPass, keyPassFile, keyPassCharset); + byte[] keyBytes = new byte[0]; + try (BufferedInputStream keyIs = + new BufferedInputStream( + new FileInputStream(Path.of(keyPath.get()).toFile()))) { + KeyFactory keyFactory = KeyFactory.getInstance(keyType); + KeySpec keySpec; + // FIXME avoid allocating a String that holds the encoded key. This + // String may sit around on the heap until the JVM decides to do a GC + // and release it. It would be better to handle this using mutable + // structures (ex. byte[] or char[]) so that it can be explicitly + // cleared ASAP, or else use off-heap memory. + String s = new String(keyIs.readAllBytes(), Charset.forName(keyCharset)); + String pem = s.replaceAll("-----.+KEY-----", "").replaceAll("\\s+", ""); + switch (keyEncoding) { + case "PKCS1": + keyBytes = buildPkcs8KeyFromPkcs1Key(Base64.getDecoder().decode(pem)); + keySpec = new PKCS8EncodedKeySpec(keyBytes, keyType); + break; + case "PKCS8": + keyBytes = Base64.getDecoder().decode(pem); + keySpec = new PKCS8EncodedKeySpec(keyBytes, keyType); + break; + default: + throw new IllegalArgumentException( + "Unimplemented key encoding: " + keyType); + } + PrivateKey key = keyFactory.generatePrivate(keySpec); + ks.setKeyEntry( + keyAlias, + key, + kp.map(CharBuffer::array).orElse(null), + new Certificate[] {cert}); + } finally { + Arrays.fill(keyBytes, (byte) 0); + clearBuffer(kp); + } + } + // set up key manager factory KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); @@ -520,10 +575,19 @@ public static Optional provideServerSslContext( } catch (KeyStoreException | CertificateException | UnrecoverableKeyException + | InvalidKeySpecException | KeyManagementException | IOException | NoSuchAlgorithmException e) { throw new RuntimeException(e); + } finally { + if (keystore != null) { + try { + keystore.close(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } } } diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 646e45ca..7b43e105 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -4,14 +4,19 @@ cryostat.agent.baseuri= cryostat.agent.api.writes-enabled=false +cryostat.agent.webserver.host=0.0.0.0 +cryostat.agent.webserver.port=9977 + +cryostat.agent.webclient.connect.timeout-ms=1000 +cryostat.agent.webclient.response.timeout-ms=1000 +cryostat.agent.webclient.response.retry-count=3 + cryostat.agent.webclient.tls.version=TLSv1.2 cryostat.agent.webclient.tls.trust-all=false cryostat.agent.webclient.tls.verify-hostname=true cryostat.agent.webclient.tls.truststore.type=JKS cryostat.agent.webclient.tls.truststore.pass-charset=utf-8 -cryostat.agent.webclient.connect.timeout-ms=1000 -cryostat.agent.webclient.response.timeout-ms=1000 -cryostat.agent.webclient.response.retry-count=3 + cryostat.agent.webclient.tls.client-auth.cert.path= cryostat.agent.webclient.tls.client-auth.cert.type=X.509 cryostat.agent.webclient.tls.client-auth.cert.alias=identity @@ -27,16 +32,26 @@ cryostat.agent.webclient.tls.client-auth.keystore.pass-charset=utf-8 cryostat.agent.webclient.tls.client-auth.keystore.pass= cryostat.agent.webclient.tls.client-auth.keystore.type=PKCS12 cryostat.agent.webclient.tls.client-auth.key-manager.type=SunX509 -cryostat.agent.webserver.host=0.0.0.0 -cryostat.agent.webserver.port=9977 + cryostat.agent.webserver.tls.version=${cryostat.agent.webclient.tls.version} cryostat.agent.webserver.tls.keystore.pass= cryostat.agent.webserver.tls.keystore.pass-charset=utf-8 cryostat.agent.webserver.tls.keystore.file= cryostat.agent.webserver.tls.keystore.type=PKCS12 + +cryostat.agent.webserver.tls.key.alias=serverKey +cryostat.agent.webserver.tls.key.path= +cryostat.agent.webserver.tls.key.charset=utf-8 +cryostat.agent.webserver.tls.key.encoding=PKCS1 +cryostat.agent.webserver.tls.key.type=RSA +cryostat.agent.webserver.tls.key.pass.file= +cryostat.agent.webserver.tls.key.pass-charset=utf-8 +cryostat.agent.webserver.tls.key.pass= + cryostat.agent.webserver.tls.cert.alias=serverCert cryostat.agent.webserver.tls.cert.file= cryostat.agent.webserver.tls.cert.type=X.509 + cryostat.agent.webserver.credentials.user=user cryostat.agent.webserver.credentials.pass.hash-function=SHA-256 cryostat.agent.webserver.credentials.pass.length=24