From cfb338690882a8f2f222788a1d7852c2d3d47b40 Mon Sep 17 00:00:00 2001 From: Jan Heise Date: Tue, 20 Jun 2023 15:33:07 +0200 Subject: [PATCH] DataNode: CA upload/generation or load existing configured CA during startup (#15546) Co-authored-by: Lukasz Kaminski --- changelog/unreleased/pr-15546.toml | 5 + .../DataNodePreflightGeneratePeriodical.java | 22 +-- .../MongoCertSecureConfiguration.java | 8 +- .../process/OpensearchConfiguration.java | 8 + .../security/certutil/CaConfiguration.java | 42 ++++++ .../graylog/security/certutil/CaService.java | 138 ++++++++++++++++++ .../security/certutil/CertConstants.java | 1 - .../security/certutil/CertutilCert.java | 7 +- .../security/certutil/CertutilCsrSign.java | 5 +- .../security/certutil/ca/CACreator.java | 25 +++- .../security/certutil/ca/PemCaReader.java | 135 +++++++++++++++++ .../keystore/storage/KeystoreFileStorage.java | 2 + .../location/KeystoreMongoCollections.java | 9 +- .../main/java/org/graylog2/Configuration.java | 3 +- .../GraylogPreflightGeneratePeriodical.java | 126 +++++++++++++--- .../preflight/PreflightWebModule.java | 3 + .../web/resources/PreflightResource.java | 44 ++++-- .../certutil/ca/PemCaReaderTests.java | 65 +++++++++ .../src/logic/rest/FetchProvider.ts | 12 ++ .../ConfigurationWizard/CAUpload.test.tsx | 25 ++-- .../ConfigurationWizard/CAUpload.tsx | 15 +- 21 files changed, 629 insertions(+), 71 deletions(-) create mode 100644 changelog/unreleased/pr-15546.toml create mode 100644 graylog2-server/src/main/java/org/graylog/security/certutil/CaConfiguration.java create mode 100644 graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java create mode 100644 graylog2-server/src/main/java/org/graylog/security/certutil/ca/PemCaReader.java create mode 100644 graylog2-server/src/test/java/org/graylog/security/certutil/ca/PemCaReaderTests.java diff --git a/changelog/unreleased/pr-15546.toml b/changelog/unreleased/pr-15546.toml new file mode 100644 index 0000000000..6285b68faa --- /dev/null +++ b/changelog/unreleased/pr-15546.toml @@ -0,0 +1,5 @@ +type = "a" +message = "Add CA configuration to load a given CA during startup for datanode certificate provisioning." + +pulls = ["15546"] + diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java index 86b17b9416..ea250a6deb 100644 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java @@ -31,15 +31,16 @@ import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoColl import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedFileStorage; import org.graylog2.cluster.NodeNotFoundException; +import org.graylog2.cluster.NodeService; import org.graylog2.cluster.preflight.NodePreflightConfig; import org.graylog2.cluster.preflight.NodePreflightConfigService; -import org.graylog2.cluster.NodeService; import org.graylog2.plugin.periodical.Periodical; import org.graylog2.plugin.system.NodeId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import java.io.IOException; import java.security.KeyStore; @@ -59,9 +60,7 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { private final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger; private final Configuration configuration; private final SmartKeystoreStorage keystoreStorage; - - //TODO: decide on password handling - private static final String DEFAULT_PASSWORD = "admin"; + private final char[] passwordSecret; @Inject public DataNodePreflightGeneratePeriodical(final NodePreflightConfigService nodePreflightConfigService, @@ -72,7 +71,8 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { final CertChainMongoStorage certMongoStorage, final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger, final Configuration configuration, - final SmartKeystoreStorage keystoreStorage) { + final SmartKeystoreStorage keystoreStorage, + final @Named("password_secret") String passwordSecret) { this.nodePreflightConfigService = nodePreflightConfigService; this.nodeService = nodeService; this.nodeId = nodeId; @@ -84,6 +84,7 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { this.keystoreStorage = keystoreStorage; // TODO: merge with real storage this.privateKeyEncryptedStorage = new PrivateKeyEncryptedFileStorage("privateKeyFilename.cert"); + this.passwordSecret = passwordSecret.toCharArray(); } @Override @@ -96,7 +97,7 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { } else if (NodePreflightConfig.State.CONFIGURED.equals(cfg.state())) { try { var node = nodeService.byNodeId(nodeId); - var csr = csrGenerator.generateCSR(DEFAULT_PASSWORD.toCharArray(), node.getHostname(), cfg.altNames(), privateKeyEncryptedStorage); + var csr = csrGenerator.generateCSR(passwordSecret, node.getHostname(), cfg.altNames(), privateKeyEncryptedStorage); csrStorage.writeCsr(csr, nodeId.getNodeId()); LOG.info("created CSR for this node"); } catch (CSRGenerationException | IOException | NodeNotFoundException | OperatorException ex) { @@ -110,22 +111,23 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { try { final Optional certificateChain = certMongoStorage.readCertChain(nodeId.getNodeId()); if (certificateChain.isPresent()) { + final char[] secret = passwordSecret; KeyStore nodeKeystore = certificateAndPrivateKeyMerger.merge( certificateChain.get(), privateKeyEncryptedStorage, - DEFAULT_PASSWORD.toCharArray(), - configuration.getDatanodeHttpCertificatePassword().toCharArray(), + secret, + secret, CertConstants.DATANODE_KEY_ALIAS ); final KeystoreMongoLocation location = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.DATA_NODE_KEYSTORE_COLLECTION); - keystoreStorage.writeKeyStore(location, nodeKeystore, configuration.getDatanodeHttpCertificatePassword().toCharArray(), null); + keystoreStorage.writeKeyStore(location, nodeKeystore, secret, secret); //should be in one transaction, but we miss transactions... nodePreflightConfigService.changeState(nodeId.getNodeId(), NodePreflightConfig.State.STORED); } } catch (Exception ex) { - LOG.error("Config entry in signed state, but no certificate data present in Mongo"); + LOG.error("Config entry in signed state, but wrong certificate data present in Mongo"); } } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java index 3a595b0d21..a87fda7d51 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java @@ -56,7 +56,7 @@ public final class MongoCertSecureConfiguration extends SecureConfiguration { private final char[] secret; private final KeystoreMongoLocation mongoLocation; - private final String mongoKeystorePassword; + private final char[] mongoKeystorePassword; @Inject public MongoCertSecureConfiguration(final Configuration localConfiguration, @@ -86,7 +86,7 @@ public final class MongoCertSecureConfiguration extends SecureConfiguration { this.secret = passwordSecret.toCharArray(); //TODO: matches line 123 of DataNodePreflightGeneratePeriodical, but both need to be changed - this.mongoKeystorePassword = localConfiguration.getDatanodeHttpCertificatePassword(); + this.mongoKeystorePassword = secret; } @Override @@ -102,11 +102,11 @@ public final class MongoCertSecureConfiguration extends SecureConfiguration { final String truststorePassword = UUID.randomUUID().toString(); keystoreReEncryption.reEncyptWithSecret( mongoLocation, - mongoKeystorePassword.toCharArray(), + mongoKeystorePassword, finalTransportKeystoreLocation); keystoreReEncryption.reEncyptWithSecret( mongoLocation, - mongoKeystorePassword.toCharArray(), + mongoKeystorePassword, finalHttpKeystoreLocation); configureInitialAdmin(localConfiguration, localConfiguration.getRestApiUsername(), localConfiguration.getRestApiPassword()); diff --git a/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java b/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java index 6aaa88c364..06de3d8f6b 100644 --- a/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java @@ -16,6 +16,7 @@ */ package org.graylog.datanode.process; +import org.apache.commons.exec.OS; import org.graylog.datanode.management.Environment; import org.graylog.shaded.opensearch2.org.apache.http.HttpHost; @@ -37,6 +38,13 @@ public record OpensearchConfiguration( public Map asMap() { Map config = new LinkedHashMap<>(); + + // currently, startup fails on macOS without disabling this filter. + // for a description of the filter (although it's for ES), see https://www.elastic.co/guide/en/elasticsearch/reference/7.17/_system_call_filter_check.html + if(OS.isFamilyMac()) { + config.put("bootstrap.system_call_filter", "false"); + } + config.put("http.port", String.valueOf(httpPort)); config.put("transport.port", String.valueOf(transportPort)); if (clusterName != null && !clusterName.isBlank()) { diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CaConfiguration.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CaConfiguration.java new file mode 100644 index 0000000000..0369d3b4a6 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CaConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.joschi.jadconfig.Parameter; +import org.graylog2.plugin.BaseConfiguration; + +import java.nio.file.Path; + +public class CaConfiguration extends BaseConfiguration { + @Parameter(value = "ca_keystore_file") + private Path caKeystoreFile; + + @Parameter(value = "ca_password") + private String caPassword; + + public Path getCaKeystoreFile() { + return caKeystoreFile; + } + + public String getCaPassword() { + return caPassword; + } + + public boolean configuredCaExists() { + return getCaKeystoreFile() != null; + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java new file mode 100644 index 0000000000..880069aa20 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java @@ -0,0 +1,138 @@ +/* + * 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 org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.graylog.security.certutil.ca.CACreator; +import org.graylog.security.certutil.ca.PemCaReader; +import org.graylog.security.certutil.ca.exceptions.CACreationException; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.keystore.storage.SmartKeystoreStorage; +import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; +import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections; +import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; +import org.graylog2.Configuration; +import org.graylog2.bootstrap.preflight.web.resources.model.CA; +import org.graylog2.bootstrap.preflight.web.resources.model.CAType; +import org.graylog2.plugin.system.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + + +@Singleton +public class CaService { + private static final Logger LOG = LoggerFactory.getLogger(CaService.class); + public static int DEFAULT_VALIDITY = 10 * 365; + private final SmartKeystoreStorage keystoreStorage; + private final KeystoreMongoLocation mongoDbCaLocation; + private final KeystoreFileLocation manuallyProvidedCALocation; + private final NodeId nodeId; + private final CACreator caCreator; + private final PemCaReader pemCaReader; + private final CaConfiguration configuration; + private final String passwordSecret; + + @Inject + public CaService(final Configuration configuration, + final SmartKeystoreStorage keystoreStorage, + final NodeId nodeId, + final CACreator caCreator, + final PemCaReader pemCaReader, + final @Named("password_secret") String passwordSecret) { + this.keystoreStorage = keystoreStorage; + this.nodeId = nodeId; + this.caCreator = caCreator; + this.pemCaReader = pemCaReader; + this.configuration = configuration; + this.passwordSecret = configuration.getCaPassword() != null ? configuration.getCaPassword() : passwordSecret; + this.mongoDbCaLocation = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.GRAYLOG_CA_KEYSTORE_COLLECTION); + this.manuallyProvidedCALocation = new KeystoreFileLocation(configuration.getCaKeystoreFile()); + } + + public CA get() throws KeyStoreStorageException { + if(configuration.configuredCaExists()) { + return new CA("local CA", CAType.LOCAL); + } else { + var keystore = keystoreStorage.readKeyStore(mongoDbCaLocation, passwordSecret.toCharArray()); + return keystore.map(c -> new CA(nodeId.getNodeId(), CAType.GENERATED)).orElse(null); + } + } + + public void create(final Integer daysValid, char[] password) throws CACreationException, KeyStoreStorageException { + final Duration certificateValidity = Duration.ofDays(daysValid == null || daysValid == 0 ? DEFAULT_VALIDITY: daysValid); + KeyStore keyStore = caCreator.createCA(passwordSecret.toCharArray(), certificateValidity); + keystoreStorage.writeKeyStore(mongoDbCaLocation, keyStore, passwordSecret.toCharArray(), password); + LOG.debug("Generated a new CA."); + } + + public void upload(String pass, List parts) throws CACreationException { + final var password = pass == null ? null : pass.toCharArray(); + // TODO: if the upload consists of more than one file, handle accordingly + // or: decide that it's always only one file containing all certificates + try { + KeyStore keyStore = KeyStore.getInstance(PKCS12); + for(BodyPart part : parts) { + InputStream is = part.getEntityAs(InputStream.class); + byte[] bytes = is.readAllBytes(); + String pem = new String(bytes, StandardCharsets.UTF_8); + // Test, if upload is PEM file, must contain at least a certificate + if (pem.contains("-----BEGIN CERTIFICATE")) { + pemCaReader.readCA(keyStore, password, pem); + } else { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + keyStore.load(bais, password); + } + } + keystoreStorage.writeKeyStore(mongoDbCaLocation, keyStore, password, passwordSecret.toCharArray()); + } catch (IOException | KeyStoreStorageException | NoSuchAlgorithmException | CertificateException | + KeyStoreException ex) { + LOG.error("Could not write CA: " + ex.getMessage(), ex); + throw new CACreationException("Could not write CA: " + ex.getMessage(), ex); + } + } + + public void startOver() { + + } + + public Optional loadKeyStore() throws KeyStoreException, KeyStoreStorageException, NoSuchAlgorithmException { + if(configuration.configuredCaExists()) { + return keystoreStorage.readKeyStore(manuallyProvidedCALocation, configuration.getCaPassword().toCharArray()); + } else { + return keystoreStorage.readKeyStore(mongoDbCaLocation, passwordSecret.toCharArray()); + } + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertConstants.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertConstants.java index 8086b9b11b..e6aaa42e6f 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertConstants.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertConstants.java @@ -20,7 +20,6 @@ package org.graylog.security.certutil; * Place to store constants that are not a subject of user/client configuration. */ public interface CertConstants { - String KEY_GENERATION_ALGORITHM = "RSA"; String SIGNING_ALGORITHM = "SHA256withRSA"; String PKCS12 = "PKCS12"; diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java index f71ea76516..acdffb80f9 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java @@ -38,6 +38,9 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Duration; +import static org.graylog.security.certutil.CertConstants.PKCS12; + + @Command(name = "cert", description = "Manage certificates for data-node", groupNames = {"certutil"}) public class CertutilCert implements CliCommand { @@ -71,7 +74,7 @@ public class CertutilCert implements CliCommand { try { char[] password = console.readPassword("Enter CA password: "); - KeyStore caKeystore = KeyStore.getInstance("PKCS12"); + KeyStore caKeystore = KeyStore.getInstance(PKCS12); caKeystore.load(new FileInputStream(caKeystorePath.toFile()), password); final Key caPrivateKey = caKeystore.getKey("ca", password); @@ -94,7 +97,7 @@ public class CertutilCert implements CliCommand { .validity(Duration.ofDays(10 * 365)); KeyPair nodePair = CertificateGenerator.generate(req); - KeyStore nodeKeystore = KeyStore.getInstance("PKCS12"); + KeyStore nodeKeystore = KeyStore.getInstance(PKCS12); nodeKeystore.load(null, null); char[] nodeKeystorePassword = console.readPassword("Enter datanode certificate password: "); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java index 08e9ea3093..9e8be2b5f7 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java @@ -39,6 +39,9 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import static org.graylog.security.certutil.CertConstants.PKCS12; + + @Command(name = "csrsign", description = "Signs a CSR using the given CA", groupNames = {"certutil"}) public class CertutilCsrSign implements CliCommand { @Option(name = "--ca", description = "Filename for the CA keystore") @@ -78,7 +81,7 @@ public class CertutilCsrSign implements CliCommand { try { char[] password = console.readPassword("Enter CA password: "); - KeyStore caKeystore = KeyStore.getInstance("PKCS12"); + KeyStore caKeystore = KeyStore.getInstance(PKCS12); caKeystore.load(new FileInputStream(caKeystoreFilename), password); final PrivateKey caPrivateKey = (PrivateKey) caKeystore.getKey("ca", password); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/ca/CACreator.java b/graylog2-server/src/main/java/org/graylog/security/certutil/ca/CACreator.java index 8169d2eb21..59af79909e 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/ca/CACreator.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/ca/CACreator.java @@ -16,17 +16,36 @@ */ package org.graylog.security.certutil.ca; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.graylog.security.certutil.CertRequest; import org.graylog.security.certutil.CertificateGenerator; import org.graylog.security.certutil.KeyPair; import org.graylog.security.certutil.ca.exceptions.CACreationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.security.KeyStore; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.graylog.security.certutil.CertConstants.PKCS12; public class CACreator { - public KeyStore createCA(final char[] password, final Duration certificateValidity) throws CACreationException { @@ -42,7 +61,8 @@ public class CACreator { .validity(certificateValidity) ); - KeyStore caKeystore = KeyStore.getInstance("PKCS12"); + KeyStore caKeystore = KeyStore.getInstance(PKCS12); + // TODO: check, if password should be used/set caKeystore.load(null, null); caKeystore.setKeyEntry("root", @@ -59,6 +79,5 @@ public class CACreator { } catch (Exception e) { throw new CACreationException("Failed to create a Certificate Authority", e); } - } } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/ca/PemCaReader.java b/graylog2-server/src/main/java/org/graylog/security/certutil/ca/PemCaReader.java new file mode 100644 index 0000000000..97afd434e0 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/ca/PemCaReader.java @@ -0,0 +1,135 @@ +/* + * 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.ca; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; +import org.graylog.security.certutil.ca.exceptions.CACreationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class PemCaReader { + private static final Logger LOG = LoggerFactory.getLogger(PemCaReader.class); + + final static String PADDING = "-----"; + final static String BEGIN = PADDING + "BEGIN"; + final static String END = PADDING + "END"; + + X509Certificate readCert(final String cert) throws IOException, CertificateException { + Reader pemReader = new StringReader(cert); + PEMParser pemParser = new PEMParser(pemReader); + var parsedObj = pemParser.readObject(); + if (parsedObj instanceof X509Certificate) { + return (X509Certificate) parsedObj; + } else if (parsedObj instanceof X509CertificateHolder) { + return new JcaX509CertificateConverter().getCertificate( + (X509CertificateHolder) parsedObj + ); + } + return null; + } + + Optional readPrivateKey(final String key, final char[] password) { + try { + Reader reader = new StringReader(key); + PEMParser pemParser = new PEMParser(reader); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + var object = pemParser.readObject(); + + if (object instanceof PEMEncryptedKeyPair) { + PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password); + return Optional.of((RSAPrivateKey)converter.getKeyPair(((PEMEncryptedKeyPair)object).decryptKeyPair(decProv)).getPrivate()); + } else { + return Optional.of((RSAPrivateKey)converter.getKeyPair((PEMKeyPair)object).getPrivate()); + } + } catch (Exception ex) { + LOG.error("Could not decode private key from pem: " + ex.getMessage(), ex); + return Optional.empty(); + } + } + + List splitPem(String pem) { + var parts = new ArrayList(); + // if pem contains another section, split it + while(pem.contains(BEGIN) && pem.contains(END)) { + var start = pem.indexOf(BEGIN); + var end = pem.indexOf(PADDING, pem.indexOf(END) + END.length()) + PADDING.length(); + parts.add(pem.substring(start, end)); + pem = pem.substring(end); + } + return parts; + } + + Optional findCert(final List certs, final String type) { + return certs.stream().filter(c -> c.startsWith(type)).findFirst(); + } + + void addCert(KeyStore keyStore, final char[] password, RSAPrivateKey pk, String cert, String name) { + try { + var certificate = readCert(cert); + keyStore.setKeyEntry(name, + pk, + password, + new X509Certificate[]{certificate}); + } catch (Exception e) { + LOG.error("Could not find certificate: " + e.getMessage(), e); + } + } + + // TODO: secure against errors, tests + public KeyStore readCA(KeyStore keyStore, final char[] password, String pem) throws CACreationException { + try { + var certs = splitPem(pem); + + var pk = findCert(certs, "-----BEGIN RSA PRIVATE KEY").flatMap(c -> this.readPrivateKey(c, password)).orElseThrow(); + certs.remove(findCert(certs, "-----BEGIN RSA PRIVATE KEY").orElse(null)); + + var root = findCert(certs, "-----BEGIN CERTIFICATE"); + root.ifPresent(cert -> { + addCert(keyStore, password, pk, cert, "root"); + certs.remove(cert); + }); + + var ca = findCert(certs, "-----BEGIN CERTIFICATE"); + ca.ifPresent(cert -> { + addCert(keyStore, password, pk, cert, "ca"); + certs.remove(cert); + }); + + return keyStore; + } catch (Exception e) { + throw new CACreationException("Failed to create a Certificate Authority", e); + } + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java index 5700a39f19..32012bfe93 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java @@ -20,6 +20,7 @@ import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; +import javax.inject.Inject; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -32,6 +33,7 @@ public final class KeystoreFileStorage implements KeystoreStorage ALL_KEYSTORE_COLLECTIONS = List.of( - DATA_NODE_KEYSTORE_COLLECTION + DATA_NODE_KEYSTORE_COLLECTION, + GRAYLOG_CA_KEYSTORE_COLLECTION ); } diff --git a/graylog2-server/src/main/java/org/graylog2/Configuration.java b/graylog2-server/src/main/java/org/graylog2/Configuration.java index 404523e913..fd274d19e4 100644 --- a/graylog2-server/src/main/java/org/graylog2/Configuration.java +++ b/graylog2-server/src/main/java/org/graylog2/Configuration.java @@ -27,6 +27,7 @@ import com.github.joschi.jadconfig.validators.PositiveDurationValidator; import com.github.joschi.jadconfig.validators.PositiveIntegerValidator; import com.github.joschi.jadconfig.validators.PositiveLongValidator; import com.github.joschi.jadconfig.validators.StringNotBlankValidator; +import org.graylog.security.certutil.CaConfiguration; import org.graylog2.cluster.leader.AutomaticLeaderElectionService; import org.graylog2.cluster.leader.LeaderElectionMode; import org.graylog2.cluster.leader.LeaderElectionService; @@ -48,7 +49,7 @@ import java.util.Set; * Helper class to hold configuration of Graylog */ @SuppressWarnings("FieldMayBeFinal") -public class Configuration extends BaseConfiguration { +public class Configuration extends CaConfiguration { /** * Deprecated! Use isLeader() instead. diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java index 57397a95e7..ce88ce656c 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java @@ -16,11 +16,23 @@ */ package org.graylog2.bootstrap.preflight; +import okhttp3.Authenticator; +import okhttp3.Call; +import okhttp3.Credentials; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Route; +import org.graylog.security.certutil.CaConfiguration; +import org.graylog.security.certutil.CaService; import org.graylog.security.certutil.cert.CertificateChain; import org.graylog.security.certutil.cert.storage.CertChainMongoStorage; import org.graylog.security.certutil.cert.storage.CertChainStorage; import org.graylog.security.certutil.csr.CsrSigner; import org.graylog.security.certutil.csr.storage.CsrMongoStorage; +import org.graylog2.Configuration; +import org.graylog2.cluster.NodeNotFoundException; +import org.graylog2.cluster.NodeService; import org.graylog2.cluster.preflight.NodePreflightConfig; import org.graylog2.cluster.preflight.NodePreflightConfigService; import org.graylog2.plugin.periodical.Periodical; @@ -28,59 +40,113 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; -import java.io.FileInputStream; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Optional; + +import static org.graylog.security.certutil.CaService.DEFAULT_VALIDITY; @Singleton public class GraylogPreflightGeneratePeriodical extends Periodical { private static final Logger LOG = LoggerFactory.getLogger(GraylogPreflightGeneratePeriodical.class); private final NodePreflightConfigService nodePreflightConfigService; + private final NodeService nodeService; + private final CaConfiguration configuration; private final CsrMongoStorage csrStorage; private final CertChainStorage certMongoStorage; - - private String caKeystoreFilename = "datanode-ca.p12"; - private Integer DEFAULT_VALIDITY = 90; - private static final String DEFAULT_PASSWORD = "admin"; + private final CaService caService; + private final String passwordSecret; + private final OkHttpClient okHttpClient; + private OkHttpClient tmpClient; @Inject public GraylogPreflightGeneratePeriodical(final NodePreflightConfigService nodePreflightConfigService, final CsrMongoStorage csrStorage, - final CertChainMongoStorage certMongoStorage) { + final CertChainMongoStorage certMongoStorage, + final CaService caService, + final Configuration configuration, + final NodeService nodeService, + final OkHttpClient okHttpClient, + final @Named("password_secret") String passwordSecret) { this.nodePreflightConfigService = nodePreflightConfigService; this.csrStorage = csrStorage; this.certMongoStorage = certMongoStorage; + this.caService = caService; + this.passwordSecret = passwordSecret; + this.configuration = configuration; + this.nodeService = nodeService; + this.okHttpClient = okHttpClient; + buildTempHttpClient(); + } + + // building a non checking httpclient + private void buildTempHttpClient() { + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } + } + }; + + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + + OkHttpClient.Builder newBuilder = new OkHttpClient.Builder(); + newBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]); + newBuilder.hostnameVerifier((hostname, session) -> true); + newBuilder.authenticator(new Authenticator() { + public Request authenticate(Route route, Response response) throws IOException { + String credential = Credentials.basic("admin", "admin"); + return response.request().newBuilder().header("Authorization", credential).build(); + } + }); + + tmpClient = newBuilder.build(); + } catch (Exception ex) { + LOG.error("Could not create tmp okhttpclient " + ex.getMessage(), ex); + } } @Override public void doRun() { LOG.debug("checking if there are configuration steps to take care of"); - final Path caKeystorePath = Path.of(caKeystoreFilename); - - if(!Files.exists(caKeystorePath)) { - LOG.warn("mandatory keystore file does not exist."); - return; - } try { - char[] password = DEFAULT_PASSWORD.toCharArray(); - KeyStore caKeystore = KeyStore.getInstance("PKCS12"); - caKeystore.load(new FileInputStream(caKeystorePath.toFile()), password); + final var password = configuration.configuredCaExists() ? configuration.getCaPassword().toCharArray() : passwordSecret.toCharArray(); + Optional optKey = caService.loadKeyStore(); + if(optKey.isEmpty()) { + LOG.warn("No keystore available."); + return; + } + KeyStore caKeystore = optKey.get(); var caPrivateKey = (PrivateKey) caKeystore.getKey("ca", password); var caCertificate = (X509Certificate) caKeystore.getCertificate("ca"); + var rootCertificate = (X509Certificate) caKeystore.getCertificate("root"); nodePreflightConfigService.streamAll() @@ -106,14 +172,34 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { } }); - } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException | - UnrecoverableKeyException e) { + nodePreflightConfigService.streamAll() + .filter(c -> NodePreflightConfig.State.STORED.equals(c.state())) + .forEach(c -> { + try { + if(checkConnectivity(c.nodeId())) { + nodePreflightConfigService.save(c.toBuilder().state(NodePreflightConfig.State.CONNECTED).build()); + } + } catch (Exception e) { + LOG.warn("Exception trying to connect to node " + c.nodeId() + ": " + e.getMessage() + ", retrying", e); + } + }); + + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { throw new RuntimeException(e); } catch (Exception e) { throw new RuntimeException(e); } } + private boolean checkConnectivity(final String nodeId) throws NodeNotFoundException, IOException { + final var node = nodeService.byNodeId(nodeId); + Request request = new Request.Builder().url(node.getTransportAddress()).build(); + Call call = tmpClient.newCall(request); + try(Response response = call.execute()) { + return response.isSuccessful(); + } + } + @Override protected Logger getLogger() { return LOG; diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java index eb4d528301..93e70ac6fe 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java @@ -21,6 +21,8 @@ import com.google.common.util.concurrent.ServiceManager; import com.google.inject.TypeLiteral; import com.google.inject.multibindings.MapBinder; import com.google.inject.multibindings.Multibinder; +import org.graylog.security.certutil.keystore.storage.KeystoreContentMover; +import org.graylog.security.certutil.keystore.storage.SinglePasswordKeystoreContentMover; import org.graylog2.bindings.providers.MongoConnectionProvider; import org.graylog2.bootstrap.preflight.web.PreflightBoot; import org.graylog2.bootstrap.preflight.web.resources.PreflightAssetsResource; @@ -44,6 +46,7 @@ public class PreflightWebModule extends Graylog2Module { bind(ServiceManager.class).toProvider(ServiceManagerProvider.class).asEagerSingleton(); bind(MongoConnection.class).toProvider(MongoConnectionProvider.class); bind(NodeService.class).to(NodeServiceImpl.class); + bind(KeystoreContentMover.class).to(SinglePasswordKeystoreContentMover.class).asEagerSingleton(); bind(PreflightConfigService.class).to(PreflightConfigServiceImpl.class); bind(PreflightBoot.class).asEagerSingleton(); diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java index 56a0db5732..c4bd43b7c2 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java @@ -16,10 +16,15 @@ */ package org.graylog2.bootstrap.preflight.web.resources; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.ca.exceptions.CACreationException; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; import org.graylog2.audit.jersey.NoAuditEvent; import org.graylog2.bootstrap.preflight.PreflightConstants; import org.graylog2.bootstrap.preflight.web.resources.model.CA; -import org.graylog2.bootstrap.preflight.web.resources.model.CAType; import org.graylog2.bootstrap.preflight.web.resources.model.CertParameters; import org.graylog2.cluster.Node; import org.graylog2.cluster.preflight.NodePreflightConfig; @@ -27,9 +32,11 @@ import org.graylog2.cluster.preflight.NodePreflightConfigService; import org.graylog2.cluster.NodeService; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -48,17 +55,21 @@ public class PreflightResource { private final NodeService nodeService; private final NodePreflightConfigService nodePreflightConfigService; - - //TODO: hardcoded in memory for now - private static CA currentCA = null; + private final CaService caService; + private final String passwordSecret; @Inject - public PreflightResource(final NodeService nodeService, final NodePreflightConfigService nodePreflightConfigService) { + public PreflightResource(final NodeService nodeService, + final NodePreflightConfigService nodePreflightConfigService, + final CaService caService, + final @Named("password_secret") String passwordSecret) { this.nodeService = nodeService; this.nodePreflightConfigService = nodePreflightConfigService; + this.caService = caService; + this.passwordSecret = passwordSecret; } - record DataNode(String nodeId, Node.Type type, String transportAddress, NodePreflightConfig.State status, String hostname, String shortNodeId) {} + record DataNode(String nodeId, Node.Type type, String transportAddress, NodePreflightConfig.State status, String errorMsg, String hostname, String shortNodeId) {} @GET @Path("/data_nodes") @@ -68,37 +79,38 @@ public class PreflightResource { return activeDataNodes.values().stream().map(n -> { final var preflight = preflightDataNodes.get(n.getNodeId()); - return new DataNode(n.getNodeId(), n.getType(), n.getTransportAddress(), preflight != null ? preflight.state() : null, n.getHostname(), n.getShortNodeId()); + return new DataNode(n.getNodeId(), n.getType(), n.getTransportAddress(), preflight != null ? preflight.state() : null, preflight != null ? preflight.errorMsg() : null, n.getHostname(), n.getShortNodeId()); }).collect(Collectors.toList()); } @GET @Path("/ca") - public CA get() { - return currentCA; //TODO + public CA get() throws KeyStoreStorageException { + return caService.get(); } @POST @Path("/ca/create") @NoAuditEvent("No Audit Event needed") - public void createCA() { - currentCA = new CA("generated CA", CAType.GENERATED); //TODO + public void createCA() throws CACreationException, KeyStoreStorageException { + // TODO: get validity from preflight UI + caService.create(CaService.DEFAULT_VALIDITY, passwordSecret.toCharArray()); } @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("/ca/upload") @NoAuditEvent("No Audit Event needed") - @Consumes(MediaType.APPLICATION_JSON) - public void uploadCA() { //TODO: input - currentCA = new CA("uploaded CA", CAType.UPLOADED); //TODO + public String uploadCA(@FormDataParam("password") String password, @FormDataParam("files") List bodyParts) throws CACreationException { + caService.upload(password, bodyParts); + return "Ok"; } @DELETE @Path("/startOver") @NoAuditEvent("No Audit Event needed") public void startOver() { - //TODO: reset all datanodes - currentCA = null; + caService.startOver(); nodePreflightConfigService.deleteAll(); } diff --git a/graylog2-server/src/test/java/org/graylog/security/certutil/ca/PemCaReaderTests.java b/graylog2-server/src/test/java/org/graylog/security/certutil/ca/PemCaReaderTests.java new file mode 100644 index 0000000000..5958f5ca0c --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog/security/certutil/ca/PemCaReaderTests.java @@ -0,0 +1,65 @@ +/* + * 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.ca; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PemCaReaderTests { + static final String rsaKey1 = """ + -----BEGIN RSA PRIVATE KEY----- + MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp + wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 + 1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh + 3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 + pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX + GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il + AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF + L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k + X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl + U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ + 37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= + -----END RSA PRIVATE KEY----- + """; + + static final String rsaKey2 = """ + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu + KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm + o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k + TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7 + 9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy + v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs + /5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00 + -----END RSA PRIVATE KEY----- + """; + + @Test + public void decodeRsaPk1() { + var pemCaReader = new PemCaReader(); + var pk = pemCaReader.readPrivateKey(rsaKey1, null); + assertThat(pk).isNotEmpty(); + } + + @Test + public void decodeRsaPk2() { + var pemCaReader = new PemCaReader(); + var pk = pemCaReader.readPrivateKey(rsaKey2, null); + assertThat(pk).isNotEmpty(); + } +} diff --git a/graylog2-web-interface/src/logic/rest/FetchProvider.ts b/graylog2-web-interface/src/logic/rest/FetchProvider.ts index fb8b65c990..b9b39cf7f2 100644 --- a/graylog2-web-interface/src/logic/rest/FetchProvider.ts +++ b/graylog2-web-interface/src/logic/rest/FetchProvider.ts @@ -247,6 +247,18 @@ export default function fetch(method: Method, url: string, body?: any, return promise(); } +export function fetchMultiPartFormData(url: string, body?: any, requireSession: boolean = true): Promise { + const promise = () => new Builder('POST', url) + .formData(body) + .build(); + + if (requireSession) { + return queuePromiseIfNotLoggedin(promise)(); + } + + return promise(); +} + export function fetchPlainText(method, url, body) { const promise = () => new Builder(method, url) .plaintext(body) diff --git a/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.test.tsx b/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.test.tsx index 689cb4d9fe..6abc0bccee 100644 --- a/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.test.tsx +++ b/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.test.tsx @@ -19,13 +19,13 @@ import React from 'react'; import { renderPreflight, screen, waitFor } from 'wrappedTestingLibrary'; import DefaultQueryClientProvider from 'DefaultQueryClientProvider'; -import fetch from 'logic/rest/FetchProvider'; +import { fetchMultiPartFormData } from 'logic/rest/FetchProvider'; import { asMock } from 'helpers/mocking'; import UserNotification from 'preflight/util/UserNotification'; import CAUpload from './CAUpload'; -jest.mock('logic/rest/FetchProvider', () => jest.fn()); +jest.mock('logic/rest/FetchProvider', () => ({ fetchMultiPartFormData: jest.fn() })); jest.mock('preflight/util/UserNotification', () => ({ error: jest.fn(), @@ -42,13 +42,20 @@ const logger = { describe('CAUpload', () => { beforeEach(() => { - asMock(fetch).mockReturnValue(Promise.resolve()); + asMock(fetchMultiPartFormData).mockReturnValue(Promise.resolve()); }); const files = [ new File(['fileBits'], 'fileName', { type: 'application/x-pem-file' }), ]; + const formData = () => { + const f = new FormData(); + files.forEach((file) => f.append('files', file)); + + return f; + }; + const findDropZone = async () => { const dropzoneContainer = await screen.findByTestId('upload-dropzone'); @@ -63,10 +70,9 @@ describe('CAUpload', () => { userEvent.upload(dropzone, files); userEvent.click(await screen.findByRole('button', { name: /Upload CA/i })); - await waitFor(() => expect(fetch).toHaveBeenCalledWith( - 'POST', + await waitFor(() => expect(fetchMultiPartFormData).toHaveBeenCalledWith( expect.stringContaining('/api/ca/upload'), - { files }, + formData(), false, )); @@ -74,7 +80,7 @@ describe('CAUpload', () => { }); it('should show error when CA upload fails', async () => { - asMock(fetch).mockImplementation(() => Promise.reject(new Error('Error'))); + asMock(fetchMultiPartFormData).mockImplementation(() => Promise.reject(new Error('Error'))); renderPreflight( @@ -87,10 +93,9 @@ describe('CAUpload', () => { userEvent.click(await screen.findByRole('button', { name: /Upload CA/i })); - await waitFor(() => expect(fetch).toHaveBeenCalledWith( - 'POST', + await waitFor(() => expect(fetchMultiPartFormData).toHaveBeenCalledWith( expect.stringContaining('/api/ca/upload'), - { files }, + formData(), false, )); diff --git a/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.tsx b/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.tsx index fd59abd2df..1939918bb8 100644 --- a/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.tsx +++ b/graylog2-web-interface/src/preflight/components/ConfigurationWizard/CAUpload.tsx @@ -20,7 +20,7 @@ import { useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Formik, Form, Field } from 'formik'; -import fetch from 'logic/rest/FetchProvider'; +import { fetchMultiPartFormData } from 'logic/rest/FetchProvider'; import UserNotification from 'preflight/util/UserNotification'; import { Input, Icon, Dropzone, FormikInput, Button, Space } from 'preflight/components/common'; import { qualifyUrl } from 'util/URLUtils'; @@ -60,7 +60,18 @@ const File = styled.div` const DeleteIcon = styled(Icon)` cursor: pointer; `; -const submitUpload = (formValues: FormValues) => fetch('POST', qualifyUrl('/api/ca/upload'), formValues, false); + +const submitUpload = (formValues: FormValues) => { + const formData = new FormData(); + + if (formValues.password !== undefined) { + formData.append('password', formValues.password); + } + + formValues.files.forEach((file) => formData.append('files', file)); + + return fetchMultiPartFormData(qualifyUrl('/api/ca/upload'), formData, false); +}; const validate = (formValues: FormValues) => { let errors = {};