diff --git a/bin/certutil b/bin/certutil new file mode 100755 index 0000000000..06b2a1ebd1 --- /dev/null +++ b/bin/certutil @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +CMD=$1 +NOHUP=${NOHUP:=$(which nohup)} +PS=${PS:=$(which ps)} + +# default java +JAVA_CMD=${JAVA_CMD:=$(which java)} + + +if [ -n "$JAVA_HOME" ] +then + # try to use $JAVA_HOME + if [ -x "$JAVA_HOME"/bin/java ] + then + JAVA_CMD="$JAVA_HOME"/bin/java + else + die "$JAVA_HOME"/bin/java is not executable + fi +fi + +# resolve links - $0 may be a softlink +GRAYLOGCTL="$0" + +while [ -h "$GRAYLOGCTL" ]; do + ls=$(ls -ld "$GRAYLOGCTL") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + GRAYLOGCTL="$link" + else + GRAYLOGCTL=$(dirname "$GRAYLOGCTL")/"$link" + fi +done + +# take variables from environment if set +GRAYLOGCTL_DIR=${GRAYLOGCTL_DIR:=$(dirname "$GRAYLOGCTL")} +GRAYLOG_JVM_DIR="$(dirname "$GRAYLOGCTL_DIR")/jvm" +GRAYLOG_SERVER_JAR=${GRAYLOG_SERVER_JAR:=graylog.jar} +DEFAULT_JAVA_OPTS="-Dlog4j2.formatMsgNoLookups=true -Djdk.tls.acknowledgeCloseNotify=true -Xms1g -Xmx1g -XX:+UseG1GC -server -XX:-OmitStackTraceInFastThrow" + +if [ -z "$JAVA_HOME" ] && [ -d "$GRAYLOG_JVM_DIR" ]; then + echo "Using bundled JVM in $GRAYLOG_JVM_DIR" + export JAVA_HOME="$GRAYLOG_JVM_DIR" + JAVA_CMD="$GRAYLOG_JVM_DIR/bin/java" +fi + +JAVA_OPTS="${JAVA_OPTS:="$DEFAULT_JAVA_OPTS"}" + +certutil() { + echo "Running certutil $1..." + cd "$GRAYLOGCTL_DIR/.." + "${JAVA_CMD}" ${JAVA_OPTS} ${LOG4J} -jar "${GRAYLOG_SERVER_JAR}" certutil $1 +} + +case "$CMD" in + ca) + certutil "ca" + ;; + cert) + certutil "cert" + ;; + http) + certutil "http" + ;; + truststore) + certutil "truststore" + ;; + *) + echo "Usage $0 {ca|cert|http|truststore}" +esac diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/Main.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/Main.java index 7b779b118e..67b1156808 100644 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/Main.java +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/Main.java @@ -26,6 +26,7 @@ import org.graylog.security.certutil.CertutilCert; import org.graylog.security.certutil.CertutilCsr; import org.graylog.security.certutil.CertutilCsrSign; import org.graylog.security.certutil.CertutilHttp; +import org.graylog.security.certutil.CertutilTruststore; import org.graylog2.bootstrap.CliCommand; import org.graylog2.bootstrap.CliCommandsProvider; @@ -42,6 +43,7 @@ public class Main { CertutilHttp.class, CertutilCsr.class, CertutilCsrSign.class, + CertutilTruststore.class, ShowVersion.class, CliCommandHelp.class)); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilTruststore.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilTruststore.java new file mode 100644 index 0000000000..3fc2227546 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilTruststore.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.security.certutil; + +import com.github.rvesse.airline.annotations.Command; +import com.github.rvesse.airline.annotations.Option; +import org.graylog.security.certutil.console.CommandLineConsole; +import org.graylog.security.certutil.console.SystemConsole; +import org.graylog2.bootstrap.CliCommand; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Locale; + +import static org.graylog.security.certutil.CertConstants.CA_KEY_ALIAS; +import static org.graylog.security.certutil.CertConstants.PKCS12; + + +@Command(name = "truststore", description = "Manage certificates for data-node", groupNames = {"certutil"}) +public class CertutilTruststore implements CliCommand { + + @Option(name = "--ca", description = "Filename for the CA keystore") + protected String caKeystoreFilename = "datanode-ca.p12"; + + @Option(name = "--truststore", description = "Filename for the generated truststore") + protected String truststoreFilename = "datanode-truststore.p12"; + + private final CommandLineConsole console; + + public static final CommandLineConsole.Prompt PROMPT_ENTER_CA_PASSWORD = CommandLineConsole.prompt("Enter CA password: "); + public static final CommandLineConsole.Prompt PROMPT_ENTER_TRUSTSTORE_PASSWORD = CommandLineConsole.prompt("Enter datanode truststore password: "); + + public CertutilTruststore() { + this.console = new SystemConsole(); + } + + public CertutilTruststore(String caKeystoreFilename, String truststoreFilename, CommandLineConsole console) { + this.caKeystoreFilename = caKeystoreFilename; + this.truststoreFilename = truststoreFilename; + this.console = console; + } + + @Override + public void run() { + console.printLine("This tool will generate a truststore with certificate of provided certificate authority"); + + final Path caKeystorePath = Path.of(caKeystoreFilename); + + console.printLine("Using certificate authority " + caKeystorePath.toAbsolutePath()); + + try { + char[] password = console.readPassword(PROMPT_ENTER_CA_PASSWORD); + KeyStore caKeystore = KeyStore.getInstance(PKCS12); + caKeystore.load(new FileInputStream(caKeystorePath.toFile()), password); + + final X509Certificate caCertificate = (X509Certificate) caKeystore.getCertificate(CA_KEY_ALIAS); + + console.printLine("Successfully read CA from the keystore"); + console.printLine(certificateInfo(caCertificate)); + + KeyStore truststore = KeyStore.getInstance(PKCS12); + truststore.load(null, null); + + char[] truststorePassword = console.readPassword(PROMPT_ENTER_TRUSTSTORE_PASSWORD); + + truststore.setCertificateEntry(CA_KEY_ALIAS, caCertificate); + + final Path nodeKeystorePath = Path.of(truststoreFilename); + try (FileOutputStream store = new FileOutputStream(nodeKeystorePath.toFile())) { + truststore.store(store, truststorePassword); + console.printLine("Truststore with the CA certificate successfully saved into " + nodeKeystorePath.toAbsolutePath()); + } + + // TODO: provide good user-friendly error message for each exception type! + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + private static String certificateInfo(X509Certificate cert) { + return String.format(Locale.ROOT, "Subject: %s, issuer: %s, not before: %s, not after: %s", cert.getSubjectX500Principal(), cert.getIssuerX500Principal(), cert.getNotBefore(), cert.getNotAfter()); + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilTruststoreTest.java b/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilTruststoreTest.java new file mode 100644 index 0000000000..a613404195 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilTruststoreTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.security.certutil; + +import org.assertj.core.api.Assertions; +import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.CertutilCa; +import org.graylog.security.certutil.CertutilTruststore; +import org.graylog.security.certutil.console.TestableConsole; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +class CertutilTruststoreTest { + + @TempDir + static Path tempDir; + + @Test + void testGenerateTruststore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidAlgorithmParameterException, CertPathValidatorException, SignatureException, InvalidKeyException, NoSuchProviderException { + + final Path caPath = tempDir.resolve("test-ca.p12"); + final Path truststorePath = tempDir.resolve("truststore.p12"); + + final TestableConsole inputCa = TestableConsole.empty() + .silent() + .register(CertutilCa.PROMPT_ENTER_CA_PASSWORD, "asdfgh"); + final CertutilCa certutilCa = new CertutilCa(caPath.toAbsolutePath().toString(), inputCa); + certutilCa.run(); + + // now we have a ROOT + CA keypair in the keystore, let's use it to generate the keystore + + TestableConsole inputHttp = TestableConsole.empty() + .silent() + .register(CertutilTruststore.PROMPT_ENTER_CA_PASSWORD, "asdfgh") + .register(CertutilTruststore.PROMPT_ENTER_TRUSTSTORE_PASSWORD, "asdfgh"); + + CertutilTruststore certutilCert = new CertutilTruststore( + caPath.toAbsolutePath().toString(), + truststorePath.toAbsolutePath().toString(), + inputHttp); + certutilCert.run(); + + KeyStore truststore = KeyStore.getInstance("PKCS12"); + truststore.load(new FileInputStream(truststorePath.toFile()), "asdfgh".toCharArray()); + + final Certificate cert = truststore.getCertificate(CertConstants.CA_KEY_ALIAS); + Assertions.assertThat(cert) + .extracting(c -> (X509Certificate) c) + .extracting(c -> c.getSubjectX500Principal().getName()) + .isEqualTo("CN=Graylog CA"); + } +}