Skip to content

Commit

Permalink
feat(TLS): allow users to import whole truststore + pass (#461)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwangggg authored Aug 27, 2024
1 parent 6e11c94 commit 54a7136
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 22 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,20 @@ and how it advertises itself to a Cryostat server instance. Properties that requ
- [ ] `cryostat.agent.webclient.tls.version` [`String`]: the version of TLS used for the Agent's client SSL context. Default `TLSv1.2`.
- [ ] `cryostat.agent.webclient.tls.trust-all` [`boolean`]: control whether the agent trusts all certificates presented by the Cryostat server. Default `false`. This should only be overridden for development and testing purposes, never in production.
- [ ] `cryostat.agent.webclient.tls.verify-hostname` [`boolean`]: control whether the agent verifies hostnames on certificates presented by the Cryostat server. Default `true`. This should only be overridden for development and testing purposes, never in production.
- [ ] `cryostat.agent.webclient.tls.trustore.certs` [`list`]: the list of truststoreConfig objects with alias, path, and type properties for certificates to be stored in the agent's truststore. For example, 'cryostat.agent.webclient.tls.truststore.certs[0].type' would be the type of the first certificate in this list. A truststoreConfig object must contain all three properties to be a valid certificate entry.
- [ ] `cryostat.agent.webclient.tls.trustore.certs` [`list`]: the list of truststoreConfig objects with alias, path, and type properties for certificates to be stored in the agent's truststore. For example, 'cryostat.agent.webclient.tls.truststore.certs[0].type' would be the type of the first certificate in this list. A truststoreConfig object must contain all three properties to be a valid certificate entry.
- [ ] `cryostat.agent.webclient.tls.truststore.type` [`String`]: the type of truststore used for the agent's client truststore. Default `JKS`.
- [ ] `cryostat.agent.webclient.tls.truststore.path` [`String`]: the filepath to the agent's webclient truststore. This takes precedent over `cryostat.agent.webclient.tls.truststore.certs` and must be configured with the truststore's pass with `cryostat.agent.webclient.tls.truststore.pass.file` or `cryostat.agent.webclient.tls.truststore.pass`.
- [ ] `cryostat.agent.webclient.tls.truststore.pass.file` [`String`]: the filepath to the agent's client truststore's password
- [ ] `cryostat.agent.webclient.tls.truststore.pass.charset` [`String`]: the character set used by the agent's client truststore's password. Default `utf-8`.
- [ ] `cryostat.agent.webclient.tls.truststore.pass` [`String`]: the String format of the agent's client truststore's pass
- [ ] `cryostat.agent.webclient.connect.timeout-ms` [`long`]: the duration in milliseconds to wait for HTTP requests to the Cryostat server to connect. Default `1000`.
- [ ] `cryostat.agent.webclient.response.timeout-ms` [`long`]: the duration in milliseconds to wait for HTTP requests to the Cryostat server to respond. Default `1000`.
- [ ] `cryostat.agent.webserver.host` [`String`]: the internal hostname or IP address for the embedded webserver to bind to. Default `0.0.0.0`.
- [ ] `cryostat.agent.webserver.port` [`int`]: the internal port number for the embedded webserver to bind to. Default `9977`.
- [ ] `cryostat.agent.webserver.tls.version` [`String`]: the version of TLS used for the Agent's server SSL context. Default `TLSv1.2`.
- [ ] `cryostat.agent.webserver.tls.keystore.pass` [`String`]: the filepath to the HTTPS server keystore's password
- [ ] `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 file path to the HTTPS server keystore
- [ ] `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.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
Expand Down
106 changes: 104 additions & 2 deletions src/main/java/io/cryostat/agent/ConfigModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package io.cryostat.agent;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -77,6 +80,16 @@ public abstract class ConfigModule {
"cryostat.agent.webclient.connect.timeout-ms";
public static final String CRYOSTAT_AGENT_WEBCLIENT_RESPONSE_TIMEOUT_MS =
"cryostat.agent.webclient.response.timeout-ms";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH =
"cryostat.agent.webclient.tls.truststore.path";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE =
"cryostat.agent.webclient.tls.truststore.pass.file";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET =
"cryostat.agent.webclient.tls.truststore.pass-charset";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS =
"cryostat.agent.webclient.tls.truststore.pass";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE =
"cryostat.agent.webclient.tls.truststore.type";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS =
"cryostat.agent.webclient.tls.truststore.cert";
public static final Pattern CRYOSTAT_AGENT_TRUSTSTORE_PATTERN =
Expand Down Expand Up @@ -254,12 +267,78 @@ public static int provideCryostatAgentWebclientResponseTimeoutMs(Config config)
return config.getValue(CRYOSTAT_AGENT_WEBCLIENT_RESPONSE_TIMEOUT_MS, int.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH)
public static Optional<String> provideCryostatAgentWebclientTlsTruststorePath(Config config) {
return config.getOptionalValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH, String.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE)
public static Optional<BytePass> provideCryostatAgentWebclientTlsTruststorePassFromFile(
Config config) {
Optional<String> truststorePassFile =
config.getOptionalValue(
CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE, String.class);
if (truststorePassFile.isEmpty()) {
return Optional.empty();
}
try (FileInputStream passFile = new FileInputStream(truststorePassFile.get())) {
byte[] pass = passFile.readAllBytes();
Optional<BytePass> bytePass = Optional.of(new BytePass(pass));
Arrays.fill(pass, (byte) 0);
return bytePass;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET)
public static String provideCryostatAgentWebclientTlsTruststorePassCharset(Config config) {
return config.getValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET, String.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS)
public static Optional<BytePass> provideCryostatAgentWebclientTlsTruststorePass(
Config config,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE)
Optional<BytePass> truststorePass,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET) String passCharset) {
Optional<String> opt =
config.getOptionalValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS, String.class);
if (opt.isEmpty()) {
return truststorePass;
}
return Optional.of(new BytePass(opt.get(), passCharset));
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE)
public static String provideCryostatAgentWebclientTlsTruststoreType(Config config) {
return config.getValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE, String.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS)
public static List<TruststoreConfig> provideCryostatAgentWecblientTlsTruststoreCerts(
Config config) {
Config config,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS) Optional<BytePass> truststorePass,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH) Optional<String> truststorePath) {
Map<Integer, TruststoreConfig.Builder> truststoreBuilders = new HashMap<>();
List<TruststoreConfig> truststoreConfigs = new ArrayList<>();

if (!truststorePass.isEmpty() || !truststorePath.isEmpty()) {
return truststoreConfigs;
}

StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(e -> e.startsWith(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS))
.forEach(
Expand Down Expand Up @@ -302,7 +381,6 @@ public static List<TruststoreConfig> provideCryostatAgentWecblientTlsTruststoreC
}
});

List<TruststoreConfig> truststoreConfigs = new ArrayList<>();
for (TruststoreConfig.Builder builder : truststoreBuilders.values()) {
try {
truststoreConfigs.add(builder.build());
Expand Down Expand Up @@ -642,4 +720,28 @@ public static URIRange fromString(String s) {
return SITE_LOCAL;
}
}

public static class BytePass {
private final byte[] buf;

public BytePass(int len) {
this.buf = new byte[len];
}

public BytePass(byte[] s) {
this.buf = Arrays.copyOf(s, s.length);
}

public BytePass(String s, String charset) {
this.buf = Arrays.copyOf(s.getBytes(Charset.forName(charset)), s.length());
}

public byte[] get() {
return Arrays.copyOf(this.buf, this.buf.length);
}

public void clear() {
Arrays.fill(this.buf, (byte) 0);
}
}
}
65 changes: 50 additions & 15 deletions src/main/java/io/cryostat/agent/MainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
Expand Down Expand Up @@ -48,6 +51,7 @@
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import io.cryostat.agent.ConfigModule.BytePass;
import io.cryostat.agent.harvest.HarvestModule;
import io.cryostat.agent.remote.RemoteContext;
import io.cryostat.agent.remote.RemoteModule;
Expand Down Expand Up @@ -133,8 +137,15 @@ public static WebServer provideWebServer(
public static SSLContext provideClientSslContext(
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_VERSION) String clientTlsVersion,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUST_ALL) boolean trustAll,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH)
Optional<String> truststorePath,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS)
Optional<BytePass> truststorePass,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET)
String passCharset,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE) String truststoreType,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS)
List<TruststoreConfig> truststores) {
List<TruststoreConfig> truststoreCerts) {
try {
if (trustAll) {
SSLContext sslCtx = SSLContext.getInstance(clientTlsVersion);
Expand Down Expand Up @@ -177,24 +188,48 @@ public X509Certificate[] getAcceptedIssuers() {
}
}

// initialize truststore
KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore ts = KeyStore.getInstance(truststoreType);
ts.load(null, null);

for (TruststoreConfig truststore : truststores) {
// initialize truststore with user provided path and pass
if (!truststorePath.isEmpty() && !truststorePass.isEmpty()) {
Charset charset = Charset.forName(passCharset);
CharsetDecoder decoder = charset.newDecoder();
ByteBuffer byteBuffer = ByteBuffer.wrap(truststorePass.get().get());
CharBuffer charBuffer = decoder.decode(byteBuffer);
try (InputStream truststore = new FileInputStream(truststorePath.get())) {
ts.load(truststore, charBuffer.array());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
byteBuffer.clear();
charBuffer.clear();
truststorePass.get().clear();
}
} else if (!truststorePath.isEmpty() || !truststorePass.isEmpty()) {
throw new IllegalArgumentException(
String.format(
"To import a truststore, provide both the path to the truststore"
+ " and the pass, or a path to a file containing the pass"));
}

// initialize truststore with user provided certs
for (TruststoreConfig truststore : truststoreCerts) {
// load truststore with certificatesCertificate
InputStream certFile = new FileInputStream(truststore.getPath());
CertificateFactory cf = CertificateFactory.getInstance(truststore.getType());
Certificate cert = cf.generateCertificate(certFile);
if (ts.containsAlias(truststore.getType())) {
throw new IllegalStateException(
String.format(
"truststore already contains a certificate with alias"
+ " \"%s\"",
truststore.getAlias()));
try (InputStream certFile = new FileInputStream(truststore.getPath())) {
CertificateFactory cf = CertificateFactory.getInstance(truststore.getType());
Certificate cert = cf.generateCertificate(certFile);
if (ts.containsAlias(truststore.getType())) {
throw new IllegalStateException(
String.format(
"truststore already contains a certificate with alias"
+ " \"%s\"",
truststore.getAlias()));
}
ts.setCertificateEntry(truststore.getAlias(), cert);
} catch (CertificateException e) {
throw new RuntimeException(e);
}
ts.setCertificateEntry(truststore.getAlias(), cert);
certFile.close();
}

// set up trust manager factory
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/io/cryostat/agent/TruststoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ private TruststoreConfig(Builder builder) {
this.alias =
Objects.requireNonNull(
builder.alias,
"Truststore config properties must include a certificate alias");
"Imported certs for the agent's truststore must include a certificate"
+ " alias");
this.path =
Objects.requireNonNull(
builder.path,
"Truststore config properties must include a certificate path");
"Imported certs for the agent's truststore must include a certificate"
+ " path");
this.type =
Objects.requireNonNull(
builder.type,
"Truststore config properties must include a certificate type");
"Imported certs for the agent's truststore must include a certificate"
+ " type");
}

public String getAlias() {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/microprofile-config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ cryostat.agent.api.writes-enabled=false
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
Expand Down

0 comments on commit 54a7136

Please sign in to comment.