diff --git a/changelog/unreleased/pr-19536.toml b/changelog/unreleased/pr-19536.toml new file mode 100644 index 0000000000..d25f859a74 --- /dev/null +++ b/changelog/unreleased/pr-19536.toml @@ -0,0 +1,4 @@ +type = "c" +message = "Introduced DatanodeKeystore for unified handling of PKI inside data node" + +pulls = ["19536"] diff --git a/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java b/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java index f6cbf7225f..968637be7f 100644 --- a/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java +++ b/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java @@ -19,11 +19,14 @@ package org.graylog.datanode.bindings; import com.google.inject.AbstractModule; import com.google.inject.multibindings.MapBinder; import org.graylog.datanode.bootstrap.preflight.DatanodeDirectoriesLockfileCheck; +import org.graylog.datanode.bootstrap.preflight.DatanodeKeystoreCheck; import org.graylog.datanode.bootstrap.preflight.OpenSearchPreconditionsCheck; import org.graylog.datanode.bootstrap.preflight.OpensearchBinPreflightCheck; import org.graylog.datanode.bootstrap.preflight.OpensearchConfigSync; import org.graylog.datanode.bootstrap.preflight.OpensearchDataDirCompatibilityCheck; +import org.graylog2.bindings.providers.MongoConnectionProvider; import org.graylog2.bootstrap.preflight.PreflightCheck; +import org.graylog2.database.MongoConnection; public class PreflightChecksBindings extends AbstractModule { @@ -35,6 +38,11 @@ public class PreflightChecksBindings extends AbstractModule { addPreflightCheck(DatanodeDirectoriesLockfileCheck.class); addPreflightCheck(OpenSearchPreconditionsCheck.class); addPreflightCheck(OpensearchDataDirCompatibilityCheck.class); + + // Mongodb is needed for legacy datanode storage, where we want to extract the certificate chain from + // mongodb and store it in local keystore + bind(MongoConnection.class).toProvider(MongoConnectionProvider.class); + addPreflightCheck(DatanodeKeystoreCheck.class); } diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java index 31b21a776d..c3c53145a2 100644 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java @@ -18,22 +18,16 @@ package org.graylog.datanode.bootstrap.preflight; import com.google.common.collect.ImmutableList; import jakarta.inject.Inject; -import jakarta.inject.Named; import jakarta.inject.Singleton; import org.bouncycastle.operator.OperatorException; -import org.graylog.datanode.configuration.DatanodeConfiguration; -import org.graylog.security.certutil.CertConstants; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.datanode.configuration.DatanodeKeystoreException; 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.CertificateAndPrivateKeyMerger; -import org.graylog.security.certutil.csr.CsrGenerator; import org.graylog.security.certutil.csr.exceptions.CSRGenerationException; import org.graylog.security.certutil.csr.storage.CsrMongoStorage; -import org.graylog.security.certutil.keystore.storage.KeystoreMongoStorage; -import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections; -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.nodes.DataNodeDto; import org.graylog2.cluster.nodes.NodeService; @@ -47,8 +41,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; -import java.nio.file.Path; -import java.security.KeyStore; import java.util.Collections; import java.util.Objects; import java.util.Optional; @@ -62,36 +54,24 @@ public class DataNodeConfigurationPeriodical extends Periodical { private final DataNodeProvisioningService dataNodeProvisioningService; private final NodeService nodeService; private final NodeId nodeId; - private final PrivateKeyEncryptedFileStorage privateKeyEncryptedStorage; private final CsrMongoStorage csrStorage; - private final CsrGenerator csrGenerator; private final CertChainStorage certMongoStorage; - private final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger; - private final char[] passwordSecret; - private final KeystoreMongoStorage mongoKeyStorage; + private final DatanodeKeystore datanodeKeystore; @Inject public DataNodeConfigurationPeriodical(final DataNodeProvisioningService dataNodeProvisioningService, final NodeService nodeService, final NodeId nodeId, final CsrMongoStorage csrStorage, - final CsrGenerator csrGenerator, final CertChainMongoStorage certMongoStorage, - final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger, - final @Named("password_secret") String passwordSecret, - final DatanodeConfiguration datanodeConfiguration, KeystoreMongoStorage mongoKeyStorage) throws IOException { + DatanodeKeystore datanodeKeystore) { this.dataNodeProvisioningService = dataNodeProvisioningService; this.nodeService = nodeService; this.nodeId = nodeId; this.csrStorage = csrStorage; - this.csrGenerator = csrGenerator; this.certMongoStorage = certMongoStorage; - this.certificateAndPrivateKeyMerger = certificateAndPrivateKeyMerger; - // TODO: merge with real storage - this.privateKeyEncryptedStorage = new PrivateKeyEncryptedFileStorage(datanodeConfiguration.datanodeDirectories().createConfigurationFile(Path.of("privateKey.cert"))); - this.passwordSecret = passwordSecret.toCharArray(); - this.mongoKeyStorage = mongoKeyStorage; + this.datanodeKeystore = datanodeKeystore; } @Override @@ -111,7 +91,8 @@ public class DataNodeConfigurationPeriodical extends Periodical { switch (state) { case CONFIGURED -> writeCsr(c); case SIGNED -> readSignedCertificate(c); - case STARTUP_TRIGGER -> dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STARTUP_REQUESTED); + case STARTUP_TRIGGER -> + dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STARTUP_REQUESTED); } }); } @@ -121,28 +102,23 @@ public class DataNodeConfigurationPeriodical extends Periodical { LOG.error("Config entry in signed state, but no certificate data present in Mongo"); } else { try { - final Optional certificateChain = certMongoStorage.readCertChain(nodeId.getNodeId()); - if (certificateChain.isPresent()) { - final char[] secret = passwordSecret; - KeyStore nodeKeystore = certificateAndPrivateKeyMerger.merge( - certificateChain.get(), - privateKeyEncryptedStorage, - secret, - secret, - CertConstants.DATANODE_KEY_ALIAS - ); - - mongoKeyStorage.writeKeyStore(KeystoreMongoLocation.datanode(nodeId), nodeKeystore, secret, secret); - - //should be in one transaction, but we miss transactions... - dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STORED); - } + certMongoStorage.readCertChain(nodeId.getNodeId()) + .ifPresent(this::processCertificateChain); } catch (Exception ex) { LOG.error("Config entry in signed state, but wrong certificate data present in Mongo"); } } } + private void processCertificateChain(CertificateChain certificateChain) { + try { + datanodeKeystore.replaceCertificatesInKeystore(certificateChain); + dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STORED); + } catch (DatanodeKeystoreException e) { + throw new RuntimeException(e); + } + } + private void writeCsr(DataNodeProvisioningConfig cfg) { try { final var node = nodeService.byNodeId(nodeId); @@ -150,10 +126,12 @@ public class DataNodeConfigurationPeriodical extends Periodical { .addAll(Optional.ofNullable(cfg.altNames()).orElse(Collections.emptyList())) .addAll(determineAltNames()) .build(); - final var csr = csrGenerator.generateCSR(passwordSecret, node.getHostname(), altNames, privateKeyEncryptedStorage); + final PKCS10CertificationRequest csr = datanodeKeystore.createCertificateSigningRequest(node.getHostname(), altNames); csrStorage.writeCsr(csr, nodeId.getNodeId()); LOG.info("created CSR for this node"); - } catch (CSRGenerationException | IOException | NodeNotFoundException | OperatorException ex) { + } catch (CSRGenerationException | IOException | NodeNotFoundException | OperatorException | + DatanodeKeystoreException + ex) { LOG.error("error generating a CSR: " + ex.getMessage(), ex); dataNodeProvisioningService.save(cfg.asError(ex.getMessage())); } diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DatanodeKeystoreCheck.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DatanodeKeystoreCheck.java new file mode 100644 index 0000000000..8c5c2beb69 --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DatanodeKeystoreCheck.java @@ -0,0 +1,84 @@ +/* + * 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.datanode.bootstrap.preflight; + +import jakarta.inject.Inject; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.security.certutil.CertRequest; +import org.graylog.security.certutil.CertificateGenerator; +import org.graylog.security.certutil.KeyPair; +import org.graylog2.bootstrap.preflight.PreflightCheck; +import org.graylog2.bootstrap.preflight.PreflightCheckException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.KeyStore; +import java.security.Security; +import java.time.Duration; +import java.util.Optional; + +/** + * This check verifies that each datanode has a private key configured. It may be needed right now, but at the moment + * we'll start provisioning and create certificate signing requests, we may be sure that there is one private key + * available. + * ยท
+ * Additionally, this check is able to restore existing keystore persisted in mongodb. It may hold valid certificate + * that we want to reuse. Otherwise, the node would undergo signing again. + */ +public class DatanodeKeystoreCheck implements PreflightCheck { + + private static final Logger LOG = LoggerFactory.getLogger(DatanodeKeystoreCheck.class); + public static final Duration DEFAULT_SELFSIGNED_CERT_VALIDITY = Duration.ofDays(99 * 365); + + private final DatanodeKeystore datanodeKeystore; + private final LegacyDatanodeKeystoreProvider legacyDatanodeKeystoreProvider; + + @Inject + public DatanodeKeystoreCheck(DatanodeKeystore datanodeKeystore, LegacyDatanodeKeystoreProvider legacyDatanodeKeystoreProvider) { + this.datanodeKeystore = datanodeKeystore; + this.legacyDatanodeKeystoreProvider = legacyDatanodeKeystoreProvider; + } + + @Override + public void runCheck() throws PreflightCheckException { + if (!datanodeKeystore.exists()) { + LOG.info("Creating keystore for this data node"); + try { + final Optional legacyKeystore = legacyDatanodeKeystoreProvider.get(); + if (legacyKeystore.isPresent()) { // remove this branch latest with 7.0 release + LOG.info("Legacy keystore discovered, converting to local file"); + datanodeKeystore.create(legacyKeystore.get()); + legacyDatanodeKeystoreProvider.deleteLocalPrivateKey(); + } else { + datanodeKeystore.create(generateKeyPair()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + LOG.debug("Private key for this data node already exists, skipping creation."); + } + } + + private static KeyPair generateKeyPair() throws Exception { + final CertRequest certRequest = CertRequest.selfSigned(DatanodeKeystore.DATANODE_KEY_ALIAS) + .isCA(false) + .validity(DEFAULT_SELFSIGNED_CERT_VALIDITY); + + return CertificateGenerator.generate(certRequest); + } +} diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/LegacyDatanodeKeystoreProvider.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/LegacyDatanodeKeystoreProvider.java new file mode 100644 index 0000000000..f5a75df0a4 --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/LegacyDatanodeKeystoreProvider.java @@ -0,0 +1,116 @@ +/* + * 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.datanode.bootstrap.preflight; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.graylog.datanode.configuration.DatanodeConfiguration; +import org.graylog.datanode.configuration.DatanodeDirectories; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.datanode.configuration.DatanodeKeystoreException; +import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; +import org.graylog2.cluster.certificates.CertificatesService; +import org.graylog2.plugin.system.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +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.UnrecoverableKeyException; +import java.util.Base64; +import java.util.Optional; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + +/** + * Remove in 7.0 release. All legacy mongodb stored keystores for datanodes should be migrated to local files + * or recreated from scratch. + */ +@Deprecated(forRemoval = true) +public class LegacyDatanodeKeystoreProvider { + + private static final Logger LOG = LoggerFactory.getLogger(LegacyDatanodeKeystoreProvider.class); + + private final CertificatesService certificatesService; + private final NodeId nodeId; + private final String passwordSecret; + + private final DatanodeDirectories datanodeDirectories; + + @Inject + public LegacyDatanodeKeystoreProvider(CertificatesService certificatesService, NodeId nodeId, final @Named("password_secret") String passwordSecret, DatanodeConfiguration datanodeConfiguration) { + this.certificatesService = certificatesService; + this.nodeId = nodeId; + this.passwordSecret = passwordSecret; + this.datanodeDirectories = datanodeConfiguration.datanodeDirectories(); + } + + public Optional get() throws KeyStoreStorageException { + return loadKeystore().filter(this::isValidKeyAndCert); + } + + private boolean isValidKeyAndCert(KeyStore keystore) { + try { + return hasPrivateKey(keystore) && DatanodeKeystore.isSignedCertificateChain(keystore); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | + DatanodeKeystoreException e) { + LOG.warn("Failed to obtain legacy keystore, ignoring it", e); + return false; + } + } + + private boolean hasPrivateKey(KeyStore keystore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return keystore.getKey(CertConstants.DATANODE_KEY_ALIAS, passwordSecret.toCharArray()) != null; + } + + private Optional loadKeystore() throws KeyStoreStorageException { + final Optional keystoreAsString = readEncodedCertFromDatabase(); + if (keystoreAsString.isPresent()) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(keystoreAsString.get()))) { + KeyStore keyStore = KeyStore.getInstance(PKCS12); + keyStore.load(bais, passwordSecret.toCharArray()); + return Optional.of(keyStore); + } catch (Exception ex) { + throw new KeyStoreStorageException("Failed to load keystore from Mongo collection for node " + nodeId.getNodeId(), ex); + } + } + return Optional.empty(); + } + + private Optional readEncodedCertFromDatabase() { + KeystoreMongoLocation location = KeystoreMongoLocation.datanode(nodeId); + return certificatesService.readCert(location); + } + + public void deleteLocalPrivateKey() { + final Path localPrivateKey = datanodeDirectories.getConfigurationTargetDir().resolve("privateKey.cert"); + if (Files.exists(localPrivateKey)) { + try { + Files.delete(localPrivateKey); + } catch (IOException e) { + LOG.warn("Failed to delete legacy datanode private key", e); + } + } + } +} diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java new file mode 100644 index 0000000000..f442f3d0fb --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java @@ -0,0 +1,183 @@ +/* + * 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.datanode.configuration; + +import com.google.common.eventbus.EventBus; +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.KeyPair; +import org.graylog.security.certutil.cert.CertificateChain; +import org.graylog.security.certutil.csr.CsrGenerator; +import org.graylog.security.certutil.csr.InMemoryKeystoreInformation; +import org.graylog.security.certutil.csr.exceptions.CSRGenerationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + +public class DatanodeKeystore { + + static { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + + private static final Logger LOG = LoggerFactory.getLogger(DatanodeKeystore.class); + private final DatanodeDirectories datanodeDirectories; + private final String passwordSecret; + + public static final Path DATANODE_KEYSTORE_FILE = Path.of("keystore.jks"); + public static String DATANODE_KEY_ALIAS = CertConstants.DATANODE_KEY_ALIAS; + private final EventBus eventBus; + + @Inject + public DatanodeKeystore(DatanodeConfiguration configuration, final @Named("password_secret") String passwordSecret, EventBus eventBus) { + this(configuration.datanodeDirectories(), passwordSecret, eventBus); + } + + DatanodeKeystore(DatanodeDirectories datanodeDirectories, String passwordSecret, EventBus eventBus) { + this.datanodeDirectories = datanodeDirectories; + this.passwordSecret = passwordSecret; + this.eventBus = eventBus; + } + + public synchronized boolean exists() { + return Files.exists(keystorePath()); + } + + public synchronized boolean hasSignedCertificate() throws DatanodeKeystoreException { + return isSignedCertificateChain(loadKeystore()); + } + + public synchronized static boolean isSignedCertificateChain(KeyStore keystore) throws DatanodeKeystoreException { + try { + + final Certificate[] certificateChain = keystore.getCertificateChain(DATANODE_KEY_ALIAS); + + if (certificateChain.length < 2) { + // only one cert, it's a self-signed cert! + return false; + } + try { + // let's take first cert, which is the datanode. It should be signed by a private key that belongs + // to the public key in the second certificate + certificateChain[0].verify(certificateChain[1].getPublicKey()); + return true; + } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | + NoSuchProviderException | SignatureException e) { + return false; + } + + } catch (KeyStoreException e) { + throw new DatanodeKeystoreException(e); + } + } + + @Nonnull + private Path keystorePath() { + return datanodeDirectories.getConfigurationTargetDir().resolve(DATANODE_KEYSTORE_FILE); + } + + public synchronized KeyStore create(KeyPair keyPair) throws DatanodeKeystoreException { + try { + return persistKeystore(keyPair.toKeystore(DATANODE_KEY_ALIAS, passwordSecret.toCharArray())); + } catch (Exception e) { + throw new DatanodeKeystoreException(e); + } + } + + public synchronized KeyStore create(KeyStore keystore) throws DatanodeKeystoreException { + return persistKeystore(keystore); + } + + public synchronized void replaceCertificatesInKeystore(CertificateChain certificateChain) throws DatanodeKeystoreException { + try { + final KeyStore keystore = loadKeystore(); + Key privateKey = keystore.getKey(DATANODE_KEY_ALIAS, passwordSecret.toCharArray()); + // replace the existing self-signed certificates chain with the signed chain from the event + keystore.setKeyEntry(DATANODE_KEY_ALIAS, privateKey, passwordSecret.toCharArray(), certificateChain.toCertificateChainArray()); + LOG.info("Persisting signed certificates to the datanode keystore finished"); + persistKeystore(keystore); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new DatanodeKeystoreException(e); + } + } + + private KeyStore persistKeystore(KeyStore keystore) throws DatanodeKeystoreException { + try (FileOutputStream fos = new FileOutputStream(keystorePath().toFile())) { + keystore.store(fos, passwordSecret.toCharArray()); + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new DatanodeKeystoreException(e); + } + triggerChangeEvent(keystore); + return keystore; + } + + private void triggerChangeEvent(KeyStore keystore) throws DatanodeKeystoreException { + if (isSignedCertificateChain(keystore)) { + eventBus.post(new DatanodeKeystoreChangedEvent()); + } + } + + public synchronized KeyStore loadKeystore() throws DatanodeKeystoreException { + try (FileInputStream fis = new FileInputStream(keystorePath().toFile())) { + KeyStore keystore = KeyStore.getInstance(PKCS12); + keystore.load(fis, passwordSecret.toCharArray()); + return keystore; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new DatanodeKeystoreException(e); + } + } + + public synchronized PKCS10CertificationRequest createCertificateSigningRequest(String hostname, List altNames) throws DatanodeKeystoreException, CSRGenerationException { + final InMemoryKeystoreInformation keystore = new InMemoryKeystoreInformation(loadKeystore(), passwordSecret.toCharArray()); + return CsrGenerator.generateCSR(keystore, DATANODE_KEY_ALIAS, hostname, altNames); + } + + public Date getCertificateExpiration() { + + try { + final X509Certificate datanodeCert = (X509Certificate) loadKeystore().getCertificate(DATANODE_KEY_ALIAS); + return datanodeCert.getNotAfter(); + } catch (KeyStoreException | DatanodeKeystoreException e) { + throw new RuntimeException(e); + } + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedStorage.java b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystoreChangedEvent.java similarity index 58% rename from graylog2-server/src/main/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedStorage.java rename to data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystoreChangedEvent.java index 9f89a51d95..8d037e7677 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedStorage.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystoreChangedEvent.java @@ -14,17 +14,11 @@ * along with this program. If not, see * . */ -package org.graylog.security.certutil.privatekey; +package org.graylog.datanode.configuration; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.pkcs.PKCSException; - -import java.io.IOException; -import java.security.PrivateKey; - -public interface PrivateKeyEncryptedStorage { - - void writeEncryptedKey(char[] password, PrivateKey privateKey) throws IOException, OperatorCreationException; - - PrivateKey readEncryptedKey(char[] password) throws IOException, PKCSException; +/** + * This event will be triggered every time a datanode private key or certificate changes. Other parts of the system, + * that use the keystore (opensearch process, jersey) should react to this event and refresh their security setups + */ +public class DatanodeKeystoreChangedEvent { } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystoreException.java b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystoreException.java new file mode 100644 index 0000000000..45c977f291 --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystoreException.java @@ -0,0 +1,23 @@ +/* + * 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.datanode.configuration; + +public class DatanodeKeystoreException extends Exception { + public DatanodeKeystoreException(Throwable cause) { + super(cause); + } +} diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java b/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java index ccfda147c6..827bb684f6 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java @@ -23,7 +23,7 @@ import jakarta.inject.Provider; import jakarta.inject.Singleton; import org.graylog.datanode.Configuration; import org.graylog.datanode.configuration.variants.InSecureConfiguration; -import org.graylog.datanode.configuration.variants.MongoCertSecureConfiguration; +import org.graylog.datanode.configuration.variants.LocalKeystoreSecureConfiguration; import org.graylog.datanode.configuration.variants.OpensearchSecurityConfiguration; import org.graylog.datanode.configuration.variants.SecurityConfigurationVariant; import org.graylog.datanode.configuration.variants.UploadedCertFilesSecureConfiguration; @@ -46,7 +46,7 @@ import java.util.stream.Collectors; public class OpensearchConfigurationProvider implements Provider { private final Configuration localConfiguration; private final UploadedCertFilesSecureConfiguration uploadedCertFilesSecureConfiguration; - private final MongoCertSecureConfiguration mongoCertSecureConfiguration; + private final LocalKeystoreSecureConfiguration mongoCertSecureConfiguration; private final InSecureConfiguration inSecureConfiguration; private final DatanodeConfiguration datanodeConfiguration; private final byte[] signingKey; @@ -62,7 +62,7 @@ public class OpensearchConfigurationProvider implements Provider nodeService, final @Named("password_secret") String passwordSecret, @@ -70,7 +70,7 @@ public class OpensearchConfigurationProvider implements Provider. + */ +package org.graylog.datanode.configuration.variants; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.graylog.datanode.Configuration; +import org.graylog.datanode.configuration.DatanodeConfiguration; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.datanode.configuration.DatanodeKeystoreException; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public final class LocalKeystoreSecureConfiguration extends SecureConfiguration { + private final DatanodeKeystore datanodeKeystore; + private final char[] secret; + + @Inject + public LocalKeystoreSecureConfiguration(final DatanodeKeystore datanodeKeystore, + final DatanodeConfiguration datanodeConfiguration, + final @Named("password_secret") String passwordSecret + ) { + super(datanodeConfiguration); + this.datanodeKeystore = datanodeKeystore; + this.secret = passwordSecret.toCharArray(); + } + + @Override + public boolean isConfigured(Configuration localConfiguration) { + try { + return datanodeKeystore.exists() && datanodeKeystore.hasSignedCertificate(); + } catch (DatanodeKeystoreException e) { + throw new RuntimeException(e); + } + } + + @Override + public OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException { + + final Path targetTransportKeystoreLocation = getTransportKeystoreLocation(); + final Path targetHttpKeystoreLocation = getHttpKeystoreLocation(); + + try { + final KeyStore datanodeKeystore = this.datanodeKeystore.loadKeystore(); + copy(datanodeKeystore, targetTransportKeystoreLocation, secret); + copy(datanodeKeystore, targetHttpKeystoreLocation, secret); + + return new OpensearchSecurityConfiguration( + new FilesystemKeystoreInformation(targetTransportKeystoreLocation.toAbsolutePath(), secret), + new FilesystemKeystoreInformation(targetHttpKeystoreLocation.toAbsolutePath(), secret) + ); + } catch (DatanodeKeystoreException e) { + throw new RuntimeException(e); + } + } + + private void copy(final KeyStore originalKeystore, final Path targetLocation, final char[] password) { + try (FileOutputStream fos = new FileOutputStream(targetLocation.toFile())) { + originalKeystore.store(fos, password); + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} 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 deleted file mode 100644 index 5faffd52fe..0000000000 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.datanode.configuration.variants; - -import jakarta.inject.Inject; -import jakarta.inject.Named; -import jakarta.validation.constraints.NotNull; -import org.graylog.datanode.Configuration; -import org.graylog.datanode.configuration.DatanodeConfiguration; -import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; -import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage; -import org.graylog.security.certutil.keystore.storage.KeystoreMongoStorage; -import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; -import org.graylog2.cluster.certificates.CertificatesService; -import org.graylog2.plugin.system.NodeId; - -import java.io.IOException; -import java.nio.file.Path; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.util.Optional; - -public final class MongoCertSecureConfiguration extends SecureConfiguration { - - private final NodeId nodeId; - private final CertificatesService certificatesService; - - private final char[] secret; - - private final char[] mongoKeystorePassword; - - private final KeystoreMongoStorage mongoKeyStorage; - private final KeystoreFileStorage fileKeyStorage; - - @Inject - public MongoCertSecureConfiguration(final DatanodeConfiguration datanodeConfiguration, - final NodeId nodeId, - final @Named("password_secret") String passwordSecret, - final CertificatesService certificatesService, KeystoreMongoStorage mongoKeyStore, KeystoreFileStorage fileKeyStore - ) { - super(datanodeConfiguration); - this.nodeId = nodeId; - this.certificatesService = certificatesService; - - this.secret = passwordSecret.toCharArray(); - this.mongoKeyStorage = mongoKeyStore; - this.fileKeyStorage = fileKeyStore; - - //TODO: matches line 123 of DataNodePreflightGeneratePeriodical, but both need to be changed - this.mongoKeystorePassword = secret; - } - - @Override - public boolean isConfigured(Configuration localConfiguration) { - return certificatesService.hasCert(getMongoKeystoreLocation()); - } - - @Override - public OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException { - - final Path targetTransportKeystoreLocation = getTransportKeystoreLocation(); - final Path targetHttpKeystoreLocation = getHttpKeystoreLocation(); - - - // this will take the mongodb-stored keys and persist them on a disk, in the opensearch configuration directory - final KeystoreMongoLocation mongoKeystoreLocation = getMongoKeystoreLocation(); - reEncypt(mongoKeystoreLocation, mongoKeystorePassword, targetTransportKeystoreLocation, secret); - reEncypt(mongoKeystoreLocation, mongoKeystorePassword, targetHttpKeystoreLocation, secret); - - return new OpensearchSecurityConfiguration( - new KeystoreInformation(targetTransportKeystoreLocation.toAbsolutePath(), secret), - new KeystoreInformation(targetHttpKeystoreLocation.toAbsolutePath(), secret) - ); - } - - @NotNull - private KeystoreMongoLocation getMongoKeystoreLocation() { - return KeystoreMongoLocation.datanode(nodeId); - } - - private void reEncypt(final KeystoreMongoLocation originalLocation, - final char[] originalPassword, - final Path targetLocation, - final char[] newPassword - ) throws KeyStoreStorageException { - - final Optional keyStore = mongoKeyStorage.readKeyStore(originalLocation, originalPassword); - if (keyStore.isPresent()) { - final KeyStore originalKeystore = keyStore.get(); - fileKeyStorage.writeKeyStore(targetLocation, originalKeystore, originalPassword, newPassword); - } else { - throw new KeyStoreStorageException("No keystore present in : " + originalLocation); - } - } -} diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java index 88996b80a1..7e7e3885e1 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.graylog.datanode.configuration.DatanodeConfiguration; import org.graylog.datanode.configuration.TruststoreCreator; import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,12 +54,12 @@ public class OpensearchSecurityConfiguration { private static final String TRUSTSTORE_FORMAT = "PKCS12"; private static final Path TRUSTSTORE_FILE = Path.of("datanode-truststore.p12"); - private final KeystoreInformation transportCertificate; - private final KeystoreInformation httpCertificate; - private KeystoreInformation truststore; + private final FilesystemKeystoreInformation transportCertificate; + private final FilesystemKeystoreInformation httpCertificate; + private FilesystemKeystoreInformation truststore; private String opensearchHeap; - public OpensearchSecurityConfiguration(KeystoreInformation transportCertificate, KeystoreInformation httpCertificate) { + public OpensearchSecurityConfiguration(FilesystemKeystoreInformation transportCertificate, FilesystemKeystoreInformation httpCertificate) { this.transportCertificate = transportCertificate; this.httpCertificate = httpCertificate; } @@ -156,15 +157,15 @@ public class OpensearchSecurityConfiguration { return !Objects.isNull(httpCertificate) && !Objects.isNull(transportCertificate); } - public KeystoreInformation getTransportCertificate() { + public FilesystemKeystoreInformation getTransportCertificate() { return transportCertificate; } - public KeystoreInformation getHttpCertificate() { + public FilesystemKeystoreInformation getHttpCertificate() { return httpCertificate; } - public KeystoreInformation getTruststore() { + public FilesystemKeystoreInformation getTruststore() { return truststore; } @@ -188,7 +189,7 @@ public class OpensearchSecurityConfiguration { return config.build(); } - private void logCertificateInformation(String certificateType, KeystoreInformation keystore) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + private void logCertificateInformation(String certificateType, FilesystemKeystoreInformation keystore) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { final KeyStore instance = KeyStore.getInstance(KEYSTORE_FORMAT); try (final FileInputStream is = new FileInputStream(keystore.location().toFile())) { instance.load(is, keystore.password()); diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java index 4c8e6b9ed5..a6c4eea3ce 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java @@ -23,7 +23,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.function.Supplier; -public sealed abstract class SecureConfiguration implements SecurityConfigurationVariant permits MongoCertSecureConfiguration, UploadedCertFilesSecureConfiguration { +public abstract class SecureConfiguration implements SecurityConfigurationVariant { /** * This filename is used only internally - we copy user-provided certificates to this location and diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java index 5f69575f2e..b7eb39ff8b 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java @@ -22,6 +22,7 @@ import org.graylog.datanode.Configuration; import org.graylog.datanode.configuration.DatanodeConfiguration; import org.graylog.datanode.configuration.OpensearchConfigurationException; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage; import java.io.IOException; @@ -129,8 +130,8 @@ public final class UploadedCertFilesSecureConfiguration extends SecureConfigurat targetHttpKeystoreLocation); return new OpensearchSecurityConfiguration( - new KeystoreInformation(targetTransportKeystoreLocation.toAbsolutePath(), transportOTP), - new KeystoreInformation(targetHttpKeystoreLocation.toAbsolutePath(), httpOTP) + new FilesystemKeystoreInformation(targetTransportKeystoreLocation.toAbsolutePath(), transportOTP), + new FilesystemKeystoreInformation(targetHttpKeystoreLocation.toAbsolutePath(), httpOTP) ); } diff --git a/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java b/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java index 54cc22ee05..b5ba740887 100644 --- a/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java +++ b/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java @@ -44,12 +44,13 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.server.model.Resource; import org.graylog.datanode.Configuration; -import org.graylog.datanode.configuration.variants.KeystoreInformation; import org.graylog.datanode.configuration.variants.OpensearchSecurityConfiguration; import org.graylog.datanode.opensearch.OpensearchConfigurationChangeEvent; import org.graylog.datanode.opensearch.configuration.OpensearchConfiguration; import org.graylog.datanode.rest.config.SecuredNodeAnnotationFilter; import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; +import org.graylog.security.certutil.csr.KeystoreInformation; import org.graylog2.configuration.TLSProtocolsConfiguration; import org.graylog2.plugin.inject.Graylog2Module; import org.graylog2.rest.MoreMediaTypes; @@ -259,7 +260,7 @@ public class JerseyService extends AbstractIdleService { return httpServer; } - private SSLEngineConfigurator buildSslEngineConfigurator(KeystoreInformation keystoreInformation) + private SSLEngineConfigurator buildSslEngineConfigurator(FilesystemKeystoreInformation keystoreInformation) throws GeneralSecurityException, IOException { if (keystoreInformation == null || !Files.isRegularFile(keystoreInformation.location()) || !Files.isReadable(keystoreInformation.location())) { throw new IllegalArgumentException("Unreadable to read private key"); @@ -280,7 +281,7 @@ public class JerseyService extends AbstractIdleService { return sslEngineConfigurator; } - private static KeyStore readKeystore(KeystoreInformation keystoreInformation) { + private static KeyStore readKeystore(FilesystemKeystoreInformation keystoreInformation) { try (var in = Files.newInputStream(keystoreInformation.location())) { KeyStore caKeystore = KeyStore.getInstance(CertConstants.PKCS12); caKeystore.load(in, keystoreInformation.password()); diff --git a/data-node/src/test/java/org/graylog/datanode/configuration/DatanodeKeystoreTest.java b/data-node/src/test/java/org/graylog/datanode/configuration/DatanodeKeystoreTest.java new file mode 100644 index 0000000000..8f89a8b9f9 --- /dev/null +++ b/data-node/src/test/java/org/graylog/datanode/configuration/DatanodeKeystoreTest.java @@ -0,0 +1,95 @@ +/* + * 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.datanode.configuration; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import jakarta.annotation.Nonnull; +import org.assertj.core.api.Assertions; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.graylog.security.certutil.CertRequest; +import org.graylog.security.certutil.CertificateGenerator; +import org.graylog.security.certutil.KeyPair; +import org.graylog.security.certutil.cert.CertificateChain; +import org.graylog.security.certutil.csr.CsrSigner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; + +class DatanodeKeystoreTest { + + private EventBus eventBus; + private final List receivedEvents = new LinkedList<>(); + + @BeforeEach + void setUp() { + eventBus = new EventBus(); + eventBus.register(this); + } + + @AfterEach + void tearDown() { + eventBus.unregister(this); + } + + @Subscribe + public void subscribe(DatanodeKeystoreChangedEvent event) { + // remember received events so we can verify them later + receivedEvents.add(event); + } + + @Test + void testCreateRead(@TempDir Path tempDir) throws Exception { + final DatanodeKeystore datanodeKeystore = new DatanodeKeystore(new DatanodeDirectories(tempDir, tempDir, tempDir, tempDir), "foobar", this.eventBus); + Assertions.assertThat(datanodeKeystore.exists()).isFalse(); + + final KeyPair keyPair = generateKeyPair(); + + datanodeKeystore.create(keyPair); + Assertions.assertThat(datanodeKeystore.exists()).isTrue(); + + Assertions.assertThat(datanodeKeystore.hasSignedCertificate()).isFalse(); + final PKCS10CertificationRequest csr = datanodeKeystore.createCertificateSigningRequest("my-hostname", List.of("second-hostname")); + Assertions.assertThat(csr.getSubject().toString()).isEqualTo("CN=my-hostname"); + + final CsrSigner signer = new CsrSigner(); + final KeyPair ca = CertificateGenerator.generate(CertRequest.selfSigned("Graylog CA").isCA(true).validity(Duration.ofDays(365))); + final X509Certificate datanodeCert = signer.sign(ca.privateKey(), ca.certificate(), csr, 30); + final CertificateChain certChain = new CertificateChain(datanodeCert, List.of(ca.certificate())); + + datanodeKeystore.replaceCertificatesInKeystore(certChain); + + Assertions.assertThat(this.receivedEvents).hasSize(1); + + Assertions.assertThat(datanodeKeystore.hasSignedCertificate()).isTrue(); + } + + @Nonnull + private static KeyPair generateKeyPair() throws Exception { + final CertRequest certRequest = CertRequest.selfSigned(DatanodeKeystore.DATANODE_KEY_ALIAS) + .isCA(false) + .validity(Duration.ofDays(31)); + return CertificateGenerator.generate(certRequest); + } +} diff --git a/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java b/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java index df9d120c46..2dc039baa0 100644 --- a/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java +++ b/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java @@ -25,8 +25,8 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.graylog.datanode.configuration.variants.KeystoreInformation; import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -56,10 +56,10 @@ public class TruststoreCreatorTest { @Test void testTrustStoreCreation(@TempDir Path tempDir) throws Exception { - final KeystoreInformation root = createKeystore(tempDir.resolve("root.p12"), "root","CN=ROOT", BigInteger.ONE); - final KeystoreInformation boot = createKeystore(tempDir.resolve("boot.p12"), "boot","CN=BOOT", BigInteger.TWO); + final FilesystemKeystoreInformation root = createKeystore(tempDir.resolve("root.p12"), "root","CN=ROOT", BigInteger.ONE); + final FilesystemKeystoreInformation boot = createKeystore(tempDir.resolve("boot.p12"), "boot","CN=BOOT", BigInteger.TWO); - final KeystoreInformation truststore = TruststoreCreator.newTruststore() + final FilesystemKeystoreInformation truststore = TruststoreCreator.newTruststore() .addRootCert("root", root, "root") .addRootCert("boot", boot, "boot") .persist(tempDir.resolve("truststore.sec"), "caramba! caramba!".toCharArray()); @@ -92,7 +92,7 @@ public class TruststoreCreatorTest { assertEquals(cnName, x509Certificate.getIssuerX500Principal().getName()); } - private KeystoreInformation createKeystore(Path path, String alias, final String cnName, final BigInteger serialNumber) throws GeneralSecurityException, OperatorCreationException, IOException { + private FilesystemKeystoreInformation createKeystore(Path path, String alias, final String cnName, final BigInteger serialNumber) throws GeneralSecurityException, OperatorCreationException, IOException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); java.security.KeyPair certKeyPair = keyGen.generateKeyPair(); X500Name name = new X500Name(cnName); @@ -121,6 +121,6 @@ public class TruststoreCreatorTest { try (final FileOutputStream fileOutputStream = new FileOutputStream(path.toFile())) { trustStore.store(fileOutputStream, password); } - return new KeystoreInformation(path, password); + return new FilesystemKeystoreInformation(path, password); } } diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java index 830023fcc3..d39ca14b18 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java @@ -19,7 +19,7 @@ package org.graylog.datanode.integration; import com.github.rholder.retry.RetryException; import jakarta.validation.constraints.NotNull; import org.apache.commons.lang3.RandomStringUtils; -import org.graylog.datanode.configuration.variants.KeystoreInformation; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.graylog.testing.restoperations.DatanodeOpensearchWait; import org.graylog.testing.restoperations.DatanodeRestApiWait; import org.graylog.testing.restoperations.DatanodeStatusChangeOperation; @@ -60,7 +60,7 @@ public class DatanodeClusterIT { static Path tempDir; private String hostnameNodeA; private KeyStore trustStore; - private KeystoreInformation ca; + private FilesystemKeystoreInformation ca; private Network network; private MongoDBTestService mongoDBTestService; @BeforeEach @@ -71,8 +71,8 @@ public class DatanodeClusterIT { trustStore = DatanodeSecurityTestUtils.buildTruststore(ca); hostnameNodeA = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); - final KeystoreInformation transportNodeA = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeA); - final KeystoreInformation httpNodeA = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeA); + final FilesystemKeystoreInformation transportNodeA = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeA); + final FilesystemKeystoreInformation httpNodeA = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeA); this.network = Network.newNetwork(); this.mongoDBTestService = MongoDBTestService.create(MongodbServer.DEFAULT_VERSION, network); @@ -88,8 +88,8 @@ public class DatanodeClusterIT { final String hostnameNodeB = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); - final KeystoreInformation transportNodeB = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeB); - final KeystoreInformation httpNodeB = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeB); + final FilesystemKeystoreInformation transportNodeB = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeB); + final FilesystemKeystoreInformation httpNodeB = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeB); nodeB = createDatanodeContainer( network, @@ -126,8 +126,8 @@ public class DatanodeClusterIT { void testAddingNodeToExistingCluster() throws ExecutionException, RetryException { final String hostnameNodeC = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); - final KeystoreInformation transportNodeC = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeC); - final KeystoreInformation httpNodeC = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeC); + final FilesystemKeystoreInformation transportNodeC = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeC); + final FilesystemKeystoreInformation httpNodeC = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeC); nodeC = createDatanodeContainer( network, mongoDBTestService, @@ -146,8 +146,8 @@ public class DatanodeClusterIT { void testRemovingNodeReallocatesShards() throws ExecutionException, RetryException { final String hostnameNodeC = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); - final KeystoreInformation transportNodeC = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeC); - final KeystoreInformation httpNodeC = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeC); + final FilesystemKeystoreInformation transportNodeC = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeC); + final FilesystemKeystoreInformation httpNodeC = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeC); nodeC = createDatanodeContainer( network, mongoDBTestService, @@ -204,8 +204,8 @@ public class DatanodeClusterIT { private DatanodeContainerizedBackend createDatanodeContainer(Network network, MongoDBTestService mongodb, String hostname, - KeystoreInformation transportKeystore, - KeystoreInformation httpKeystore) { + FilesystemKeystoreInformation transportKeystore, + FilesystemKeystoreInformation httpKeystore) { return new DatanodeContainerizedBackend( network, mongodb, diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeLifecycleIT.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeLifecycleIT.java index 425662e1da..76f0d29aba 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeLifecycleIT.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeLifecycleIT.java @@ -17,11 +17,11 @@ package org.graylog.datanode.integration; import org.apache.commons.lang3.RandomStringUtils; -import org.graylog.datanode.configuration.variants.KeystoreInformation; +import org.graylog.datanode.testinfra.DatanodeContainerizedBackend; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.graylog.testing.restoperations.DatanodeRestApiWait; import org.graylog.testing.restoperations.DatanodeStatusChangeOperation; import org.graylog.testing.restoperations.RestOperationParameters; -import org.graylog.datanode.testinfra.DatanodeContainerizedBackend; import org.graylog2.plugin.Tools; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -51,13 +51,13 @@ public class DatanodeLifecycleIT { void setUp() throws IOException, GeneralSecurityException { containerHostname = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); // first generate a self-signed CA - KeystoreInformation ca = DatanodeSecurityTestUtils.generateCa(tempDir); + FilesystemKeystoreInformation ca = DatanodeSecurityTestUtils.generateCa(tempDir); trustStore = DatanodeSecurityTestUtils.buildTruststore(ca); // use the CA to generate transport certificate keystore - final KeystoreInformation transportCert = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, containerHostname); + final FilesystemKeystoreInformation transportCert = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, containerHostname); // use the CA to generate HTTP certificate keystore - final KeystoreInformation httpCert = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, containerHostname, Tools.getLocalCanonicalHostname()); + final FilesystemKeystoreInformation httpCert = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, containerHostname, Tools.getLocalCanonicalHostname()); backend = new DatanodeContainerizedBackend(containerHostname, datanodeContainer -> { // provide the keystore files to the docker container diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java index 3d1e552fa6..b2d8b5433c 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java @@ -19,10 +19,10 @@ package org.graylog.datanode.integration; import com.github.rholder.retry.RetryException; import io.restassured.response.ValidatableResponse; import org.apache.commons.lang3.RandomStringUtils; -import org.graylog.datanode.configuration.variants.KeystoreInformation; +import org.graylog.datanode.testinfra.DatanodeContainerizedBackend; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.graylog.testing.restoperations.DatanodeRestApiWait; import org.graylog.testing.restoperations.RestOperationParameters; -import org.graylog.datanode.testinfra.DatanodeContainerizedBackend; import org.graylog2.plugin.Tools; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; @@ -54,13 +54,13 @@ public class DatanodeSecuritySetupIT { void setUp() throws IOException, GeneralSecurityException { containerHostname = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); // first generate a self-signed CA - KeystoreInformation ca = DatanodeSecurityTestUtils.generateCa(tempDir); + FilesystemKeystoreInformation ca = DatanodeSecurityTestUtils.generateCa(tempDir); trustStore = DatanodeSecurityTestUtils.buildTruststore(ca); // use the CA to generate transport certificate keystore - final KeystoreInformation transportCert = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, containerHostname); + final FilesystemKeystoreInformation transportCert = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, containerHostname); // use the CA to generate HTTP certificate keystore - final KeystoreInformation httpCert = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, containerHostname, Tools.getLocalCanonicalHostname()); + final FilesystemKeystoreInformation httpCert = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, containerHostname, Tools.getLocalCanonicalHostname()); backend = new DatanodeContainerizedBackend(containerHostname, datanodeContainer -> { // provide the keystore files to the docker container diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecurityTestUtils.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecurityTestUtils.java index b19c2b27c1..64609c0eaf 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecurityTestUtils.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecurityTestUtils.java @@ -17,11 +17,11 @@ package org.graylog.datanode.integration; import org.apache.commons.lang3.RandomStringUtils; -import org.graylog.datanode.configuration.variants.KeystoreInformation; import org.graylog.security.certutil.CertutilCa; import org.graylog.security.certutil.CertutilCert; import org.graylog.security.certutil.CertutilHttp; import org.graylog.security.certutil.console.TestableConsole; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import java.io.FileInputStream; import java.io.IOException; @@ -33,7 +33,7 @@ import java.security.cert.X509Certificate; import java.util.Enumeration; public class DatanodeSecurityTestUtils { - public static KeyStore buildTruststore(KeystoreInformation ca) throws IOException, GeneralSecurityException { + public static KeyStore buildTruststore(FilesystemKeystoreInformation ca) throws IOException, GeneralSecurityException { try (FileInputStream fis = new FileInputStream(ca.location().toFile())) { KeyStore caKeystore = KeyStore.getInstance("PKCS12"); @@ -54,17 +54,17 @@ public class DatanodeSecurityTestUtils { } } - public static KeystoreInformation generateCa(Path dir) { + public static FilesystemKeystoreInformation generateCa(Path dir) { final Path certPath = dir.resolve("test-ca.p12"); final String password = RandomStringUtils.randomAlphabetic(10); final TestableConsole input = TestableConsole.empty().silent() .register(CertutilCa.PROMPT_ENTER_CA_PASSWORD, password); final CertutilCa command = new CertutilCa(certPath.toAbsolutePath().toString(), input); command.run(); - return new KeystoreInformation(certPath, password); + return new FilesystemKeystoreInformation(certPath, password); } - public static KeystoreInformation generateTransportCert(Path dir, KeystoreInformation ca, String... containerHostnames) { + public static FilesystemKeystoreInformation generateTransportCert(Path dir, FilesystemKeystoreInformation ca, String... containerHostnames) { final Path transportPath = dir.resolve("transport-" + RandomStringUtils.randomAlphabetic(10) + ".p12"); final String password = RandomStringUtils.randomAlphabetic(10); TestableConsole inputCert = TestableConsole.empty().silent() @@ -76,10 +76,10 @@ public class DatanodeSecurityTestUtils { transportPath.toAbsolutePath().toString(), inputCert); certutilCert.run(); - return new KeystoreInformation(transportPath, password); + return new FilesystemKeystoreInformation(transportPath, password); } - public static KeystoreInformation generateHttpCert(Path dir, KeystoreInformation ca, String... containerHostnames) { + public static FilesystemKeystoreInformation generateHttpCert(Path dir, FilesystemKeystoreInformation ca, String... containerHostnames) { final Path httpPath = dir.resolve("http-" + RandomStringUtils.randomAlphabetic(10) + ".p12"); final String password = RandomStringUtils.randomAlphabetic(10); TestableConsole inputHttp = TestableConsole.empty().silent() @@ -93,6 +93,6 @@ public class DatanodeSecurityTestUtils { httpPath.toAbsolutePath().toString(), inputHttp); certutilCert.run(); - return new KeystoreInformation(httpPath, password); + return new FilesystemKeystoreInformation(httpPath, password); } } 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 57f650aa06..c7d590fd10 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 @@ -23,6 +23,7 @@ public interface CertConstants { String KEY_GENERATION_ALGORITHM = "RSA"; String SIGNING_ALGORITHM = "SHA256withRSA"; String PKCS12 = "PKCS12"; + @Deprecated String DATANODE_KEY_ALIAS = "datanode"; String CA_KEY_ALIAS = "ca"; } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsr.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsr.java index 419aae45ce..dc900df8f5 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsr.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsr.java @@ -22,62 +22,66 @@ import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.graylog.security.certutil.console.CommandLineConsole; import org.graylog.security.certutil.console.SystemConsole; import org.graylog.security.certutil.csr.CsrGenerator; +import org.graylog.security.certutil.csr.FilesystemKeystoreInformation; import org.graylog.security.certutil.csr.storage.CsrFileStorage; import org.graylog.security.certutil.csr.storage.CsrStorage; -import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedFileStorage; -import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; import org.graylog2.bootstrap.CliCommand; +import java.io.FileOutputStream; import java.nio.file.Path; +import java.security.KeyStore; +import java.time.Duration; import java.util.List; @Command(name = "csr", description = "Create CSR", groupNames = {"certutil"}) public class CertutilCsr implements CliCommand { - @Option(name = "--privateKey", description = "Keystore with private key", typeConverterProvider = PathTypeConverterProvider.class ) - protected Path privateKeyFilename = Path.of("csr-private-key.key"); + @Option(name = "--keystore", description = "Keystore with private key", typeConverterProvider = PathTypeConverterProvider.class) + protected Path keystore = Path.of("keystore.jks"); - @Option(name = "--csrFile", description = "Keystore with private key") + @Option(name = "--csrFile", description = "Certificate signing request file") protected String csrFilename = "csr.csr"; private final CommandLineConsole console; - private final PrivateKeyEncryptedStorage privateKeyEncryptedStorage; private final CsrStorage csrStorage; - private final CsrGenerator csrGenerator; public static final CommandLineConsole.Prompt PROMPT_ENTER_PASSWORD_TO_PROTECT_YOUR_PRIVATE_KEY = CommandLineConsole.prompt("Enter password to protect your private key : "); public CertutilCsr() { this.console = new SystemConsole(); - this.privateKeyEncryptedStorage = new PrivateKeyEncryptedFileStorage(privateKeyFilename); this.csrStorage = new CsrFileStorage(csrFilename); - this.csrGenerator = new CsrGenerator(); } - public CertutilCsr(final Path privateKeyFilename, + public CertutilCsr(final Path keystore, final String csrFilename, final CommandLineConsole console) { - this.privateKeyFilename = privateKeyFilename; + this.keystore = keystore; this.csrFilename = csrFilename; this.console = console; - this.privateKeyEncryptedStorage = new PrivateKeyEncryptedFileStorage(privateKeyFilename); this.csrStorage = new CsrFileStorage(csrFilename); - this.csrGenerator = new CsrGenerator(); } @Override public void run() { console.printLine("This tool will generate a CSR for the datanode"); char[] privKeyPassword = this.console.readPassword(PROMPT_ENTER_PASSWORD_TO_PROTECT_YOUR_PRIVATE_KEY); - try { + final KeyPair keyPair = CertificateGenerator.generate(CertRequest.selfSigned(CertConstants.DATANODE_KEY_ALIAS).isCA(false).validity(Duration.ofDays(99 * 365))); + + final KeyStore keystore = keyPair.toKeystore(CertConstants.DATANODE_KEY_ALIAS, privKeyPassword); + try (FileOutputStream fos = new FileOutputStream(this.keystore.toFile())) { + keystore.store(fos, privKeyPassword); + } + + final FilesystemKeystoreInformation keystoreInformation = new FilesystemKeystoreInformation(this.keystore, privKeyPassword); + console.printLine("Generating CSR for the datanode"); - final PKCS10CertificationRequest csr = csrGenerator.generateCSR( - privKeyPassword, + final PKCS10CertificationRequest csr = CsrGenerator.generateCSR( + keystoreInformation, + CertConstants.DATANODE_KEY_ALIAS, "localhost", - List.of("data-node"), - privateKeyEncryptedStorage); + List.of("data-node")); csrStorage.writeCsr(csr); } catch (Exception e) { throw new RuntimeException(e); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/KeyPair.java b/graylog2-server/src/main/java/org/graylog/security/certutil/KeyPair.java index f26239148d..d23141513d 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/KeyPair.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/KeyPair.java @@ -16,5 +16,21 @@ */ package org.graylog.security.certutil; -public record KeyPair(java.security.PrivateKey privateKey, java.security.PublicKey publicKey, java.security.cert.X509Certificate certificate) { +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + +public record KeyPair(java.security.PrivateKey privateKey, java.security.PublicKey publicKey, + java.security.cert.X509Certificate certificate) { + public KeyStore toKeystore(String alias, char[] password) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + KeyStore keystore = KeyStore.getInstance(PKCS12); + keystore.load(null, null); + keystore.setKeyEntry(alias, privateKey(), password, new Certificate[]{certificate()}); + return keystore; + } } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CertificateAndPrivateKeyMerger.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CertificateAndPrivateKeyMerger.java deleted file mode 100644 index e8d68397a4..0000000000 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CertificateAndPrivateKeyMerger.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.csr; - -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.pkcs.PKCSException; -import org.graylog.security.certutil.CertConstants; -import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; -import org.graylog.security.certutil.cert.CertificateChain; -import org.graylog.security.certutil.keystore.storage.KeystoreUtils; -import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.PrivateKey; - -/** - * Merges signed certificate, received after CSR was processed, with private key, that was awaiting in a safe file. - */ -public class CertificateAndPrivateKeyMerger { - - public KeyStore merge(final CertificateChain certificateChain, - final PrivateKeyEncryptedStorage privateKeyEncryptedStorage, - final char[] privateKeyStoragePassword, - final char[] certFilePassword, - final String alias) - throws GeneralSecurityException, IOException, OperatorCreationException, PKCSException, KeyStoreStorageException { - - KeyStore nodeKeystore = KeyStore.getInstance(CertConstants.PKCS12); - nodeKeystore.load(null, null); - - final PrivateKey privateKey = privateKeyEncryptedStorage.readEncryptedKey(privateKeyStoragePassword); - if (!KeystoreUtils.matchingKeys(privateKey, certificateChain.signedCertificate().getPublicKey())) { - throw new GeneralSecurityException("Private key from CSR and public key from certificate do not form a valid pair"); - } - nodeKeystore.setKeyEntry(alias, privateKey, certFilePassword, certificateChain.toCertificateChainArray()); - - return nodeKeystore; - } -} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/ClientCertGenerator.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/ClientCertGenerator.java index b9906dadc5..d268afb38e 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/ClientCertGenerator.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/ClientCertGenerator.java @@ -20,6 +20,10 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.CertConstants; +import org.graylog.security.certutil.CertRequest; +import org.graylog.security.certutil.CertificateGenerator; +import org.graylog.security.certutil.KeyPair; import org.graylog.security.certutil.csr.exceptions.ClientCertGenerationException; import org.graylog2.indexer.security.SecurityAdapter; import org.graylog2.plugin.certificates.RenewalPolicy; @@ -30,6 +34,7 @@ import java.io.StringWriter; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; +import java.time.Duration; import java.util.List; import static org.graylog.security.certutil.CertConstants.CA_KEY_ALIAS; @@ -37,7 +42,6 @@ import static org.graylog.security.certutil.CertConstants.CA_KEY_ALIAS; public class ClientCertGenerator { private final CaService caService; private final String passwordSecret; - private final CsrGenerator csrGenerator; private final CsrSigner csrSigner; private final ClusterConfigService clusterConfigService; private final SecurityAdapter securityAdapter; @@ -45,13 +49,11 @@ public class ClientCertGenerator { @Inject public ClientCertGenerator(final CaService caService, @Named("password_secret") final String passwordSecret, - final CsrGenerator csrGenerator, final CsrSigner csrSigner, final ClusterConfigService clusterConfigService, final SecurityAdapter securityAdapter) { this.caService = caService; this.passwordSecret = passwordSecret; - this.csrGenerator = csrGenerator; this.csrSigner = csrSigner; this.clusterConfigService = clusterConfigService; this.securityAdapter = securityAdapter; @@ -65,17 +67,22 @@ public class ClientCertGenerator { final KeyStore caKeystore = caService.loadKeyStore().orElseThrow(()-> new IllegalStateException("No CA configured")); - var caPrivateKey = (PrivateKey) caKeystore.getKey(CA_KEY_ALIAS, passwordSecret.toCharArray()); + final char[] keystorePAss = passwordSecret.toCharArray(); + var caPrivateKey = (PrivateKey) caKeystore.getKey(CA_KEY_ALIAS, keystorePAss); var caCertificate = (X509Certificate) caKeystore.getCertificate(CA_KEY_ALIAS); - var privateKeyEncryptedStorage = new InMemoryPrivateKeyStorage(); - var csr = csrGenerator.generateCSR(privateKeyPassword, principal, List.of(principal), privateKeyEncryptedStorage); - var pk = privateKeyEncryptedStorage.readEncryptedKey(privateKeyPassword); + final KeyPair keyPair = CertificateGenerator.generate(CertRequest.selfSigned(principal).isCA(false).validity(Duration.ofDays(10 * 365))); + + + final KeyStore keystore = keyPair.toKeystore(CertConstants.DATANODE_KEY_ALIAS, keystorePAss); + final InMemoryKeystoreInformation keystoreInformation = new InMemoryKeystoreInformation(keystore, keystorePAss); + var csr = CsrGenerator.generateCSR(keystoreInformation, CertConstants.DATANODE_KEY_ALIAS, principal, List.of(principal)); + var cert = csrSigner.sign(caPrivateKey, caCertificate, csr, renewalPolicy); securityAdapter.addUserToRoleMapping(role, principal); - return new ClientCert(principal, role, serializeAsPEM(caCertificate), serializeAsPEM(pk), serializeAsPEM(cert)); + return new ClientCert(principal, role, serializeAsPEM(caCertificate), serializeAsPEM(keyPair.privateKey()), serializeAsPEM(cert)); } catch (Exception e) { throw new ClientCertGenerationException("Failed to generate client certificate: " + e.getMessage(), e); } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java index 83c05dfafd..388cc760d8 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java @@ -26,15 +26,15 @@ import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.csr.exceptions.CSRGenerationException; -import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; import javax.security.auth.x500.X500Principal; -import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; import java.util.ArrayList; import java.util.List; -import static org.graylog.security.certutil.CertConstants.KEY_GENERATION_ALGORITHM; import static org.graylog.security.certutil.CertConstants.SIGNING_ALGORITHM; public class CsrGenerator { @@ -42,24 +42,19 @@ public class CsrGenerator { /** * Generates new CSR. * - * @param privateKeyPassword Password to protect private key. - * @param altNames List of alternative names to be stored in CSR - * @param privateKeyEncryptedStorage Mechanism for storing private keys. + * @param altNames List of alternative names to be stored in CSR * @return A new CSR, instance of {@link PKCS10CertificationRequest} */ - public PKCS10CertificationRequest generateCSR(final char[] privateKeyPassword, - final String principalName, - final List altNames, - final PrivateKeyEncryptedStorage privateKeyEncryptedStorage) throws CSRGenerationException { + public static PKCS10CertificationRequest generateCSR(KeystoreInformation keystoreInformation, String alias, + final String principalName, + final List altNames) throws CSRGenerationException { try { - final var keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); - final var certKeyPair = keyGen.generateKeyPair(); - privateKeyEncryptedStorage.writeEncryptedKey(privateKeyPassword, certKeyPair.getPrivate()); + final KeyStore keystore = keystoreInformation.loadKeystore(); final var p10Builder = new JcaPKCS10CertificationRequestBuilder( new X500Principal("CN=" + principalName), - certKeyPair.getPublic() + keystore.getCertificate(alias).getPublicKey() ); final var names = new ArrayList<>(List.of(principalName)); @@ -81,7 +76,7 @@ public class CsrGenerator { new Extensions(subjectAltNames)); JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(SIGNING_ALGORITHM); - ContentSigner signer = csBuilder.build(certKeyPair.getPrivate()); + ContentSigner signer = csBuilder.build((PrivateKey) keystore.getKey(alias, keystoreInformation.password())); return p10Builder.build(signer); } catch (Exception e) { diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/FilesystemKeystoreInformation.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/FilesystemKeystoreInformation.java new file mode 100644 index 0000000000..fee28b0cd7 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/FilesystemKeystoreInformation.java @@ -0,0 +1,41 @@ +/* + * 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.csr; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + +public record FilesystemKeystoreInformation(Path location, char[] password) implements KeystoreInformation { + public FilesystemKeystoreInformation(Path location, String password) { + this(location, password.toCharArray()); + } + + public KeyStore loadKeystore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + KeyStore keyStore = KeyStore.getInstance(PKCS12); + try (FileInputStream fis = new FileInputStream(location.toFile())) { + keyStore.load(fis, password); + return keyStore; + } + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/InMemoryPrivateKeyStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/InMemoryKeystoreInformation.java similarity index 61% rename from graylog2-server/src/main/java/org/graylog/security/certutil/csr/InMemoryPrivateKeyStorage.java rename to graylog2-server/src/main/java/org/graylog/security/certutil/csr/InMemoryKeystoreInformation.java index f6c0b9dce8..f4f216ddaf 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/InMemoryPrivateKeyStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/InMemoryKeystoreInformation.java @@ -1,4 +1,4 @@ -/* + /* * Copyright (C) 2020 Graylog, Inc. * * This program is free software: you can redistribute it and/or modify @@ -16,21 +16,25 @@ */ package org.graylog.security.certutil.csr; -import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; +import java.security.KeyStore; -import java.security.PrivateKey; +public class InMemoryKeystoreInformation implements KeystoreInformation { -public class InMemoryPrivateKeyStorage implements PrivateKeyEncryptedStorage { + private final KeyStore keyStore; + private final char[] password; - private PrivateKey privateKey; - - @Override - public void writeEncryptedKey(char[] password, PrivateKey privateKey) { - this.privateKey = privateKey; + public InMemoryKeystoreInformation(KeyStore keyStore, char[] password) { + this.keyStore = keyStore; + this.password = password; } @Override - public PrivateKey readEncryptedKey(char[] password) { - return privateKey; + public KeyStore loadKeystore() throws Exception { + return keyStore; + } + + @Override + public char[] password() { + return password; } } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/KeystoreInformation.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/KeystoreInformation.java similarity index 69% rename from data-node/src/main/java/org/graylog/datanode/configuration/variants/KeystoreInformation.java rename to graylog2-server/src/main/java/org/graylog/security/certutil/csr/KeystoreInformation.java index 78aadebb1e..dbde0f347f 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/KeystoreInformation.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/KeystoreInformation.java @@ -14,16 +14,17 @@ * along with this program. If not, see * . */ -package org.graylog.datanode.configuration.variants; +package org.graylog.security.certutil.csr; -import java.nio.file.Path; +import java.security.KeyStore; -public record KeystoreInformation(Path location, char[] password) { - public KeystoreInformation(Path location, String password) { - this(location, password.toCharArray()); - } +public interface KeystoreInformation { - public String passwordAsString() { + KeyStore loadKeystore() throws Exception; + + char[] password(); + + default String passwordAsString() { return new String(password()); } } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedFileStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedFileStorage.java deleted file mode 100644 index 18970a25cd..0000000000 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedFileStorage.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.privatekey; - -import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.operator.InputDecryptorProvider; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; -import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfoBuilder; -import org.bouncycastle.pkcs.PKCSException; -import org.bouncycastle.pkcs.jcajce.JcaPKCS8EncryptedPrivateKeyInfoBuilder; -import org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder; -import org.bouncycastle.pkcs.jcajce.JcePKCSPBEOutputEncryptorBuilder; - -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.security.PrivateKey; -import java.security.Security; - -public record PrivateKeyEncryptedFileStorage(Path privateKeyFilename) implements PrivateKeyEncryptedStorage { - - static { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); - } - - @Override - public void writeEncryptedKey(char[] password, PrivateKey privateKey) - throws IOException, OperatorCreationException { - - JcaPEMWriter pemWriter = new JcaPEMWriter(new FileWriter(privateKeyFilename.toFile(), Charset.defaultCharset())); - PKCS8EncryptedPrivateKeyInfoBuilder pkcs8Builder = - new JcaPKCS8EncryptedPrivateKeyInfoBuilder(privateKey); - pemWriter.writeObject(pkcs8Builder.build(new JcePKCSPBEOutputEncryptorBuilder( - NISTObjectIdentifiers.id_aes256_CBC) - .setProvider(BouncyCastleProvider.PROVIDER_NAME) - .build(password))); - pemWriter.close(); - - } - - @Override - public PrivateKey readEncryptedKey(char[] password) - throws IOException, PKCSException { - PEMParser parser = new PEMParser(new FileReader(privateKeyFilename.toFile(), Charset.defaultCharset())); - PKCS8EncryptedPrivateKeyInfo encPrivKeyInfo = (PKCS8EncryptedPrivateKeyInfo) parser.readObject(); - InputDecryptorProvider pkcs8Prov = new JcePKCSPBEInputDecryptorProviderBuilder() - .setProvider(BouncyCastleProvider.PROVIDER_NAME) - .build(password); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME); - return converter.getPrivateKey(encPrivKeyInfo.decryptPrivateKeyInfo(pkcs8Prov)); - } -} diff --git a/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CertificateAndPrivateKeyMergerTest.java b/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CertificateAndPrivateKeyMergerTest.java deleted file mode 100644 index e9d976727e..0000000000 --- a/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CertificateAndPrivateKeyMergerTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.csr; - -import org.graylog.security.certutil.CertRequest; -import org.graylog.security.certutil.CertificateGenerator; -import org.graylog.security.certutil.KeyPair; -import org.graylog.security.certutil.cert.CertificateChain; -import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.security.GeneralSecurityException; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.time.Duration; -import java.util.Enumeration; -import java.util.List; - -import static org.graylog.security.certutil.CertConstants.KEY_GENERATION_ALGORITHM; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class CertificateAndPrivateKeyMergerTest { - - CertificateAndPrivateKeyMerger toTest; - - @BeforeEach - void setUp() { - toTest = new CertificateAndPrivateKeyMerger(); - } - - @Test - void testThrowsExceptionIfPrivateKeyAndCertificateDoNotMatch() throws Exception { - - KeyPairGenerator keyGen1 = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); - keyGen1.initialize(2048); - final java.security.KeyPair keyPair1 = keyGen1.genKeyPair(); - final PrivateKeyEncryptedStorage keyStorage = mockPrivateKeyStorage(keyPair1.getPrivate()); - - final CertRequest req = CertRequest.selfSigned("localhost") - .validity(Duration.ofHours(24)); - final KeyPair keyPair = CertificateGenerator.generate(req); - - assertThrows(GeneralSecurityException.class, () -> toTest.merge(new CertificateChain(keyPair.certificate(), List.of()), - keyStorage, - "privPass".toCharArray(), - "certPass".toCharArray(), - "data-node") - ); - } - - @Test - void testMergingOnRealPairImplementation() throws Exception { - final CertRequest req = CertRequest.selfSigned("localhost") - .validity(Duration.ofHours(24)); - final KeyPair keyPair = CertificateGenerator.generate(req); - - final char[] privPass = "privPass".toCharArray(); - final char[] certPass = "certPass".toCharArray(); - final String alias = "data-node"; - - final PrivateKeyEncryptedStorage storage = mockPrivateKeyStorage(keyPair.privateKey()); - - final KeyStore merged = toTest.merge(new CertificateChain(keyPair.certificate(), List.of()), - storage, - privPass, - certPass, - alias); - - assertEquals(keyPair.privateKey(), merged.getKey(alias, certPass)); - assertEquals(keyPair.certificate(), merged.getCertificate(alias)); - - final Enumeration aliases = merged.aliases(); - assertEquals(alias, aliases.nextElement()); - assertFalse(aliases.hasMoreElements()); - } - - private static PrivateKeyEncryptedStorage mockPrivateKeyStorage(PrivateKey privateKey) { - return new PrivateKeyEncryptedStorage() { - @Override - public void writeEncryptedKey(char[] password, PrivateKey privateKey) { - // do nothing - } - - @Override - public PrivateKey readEncryptedKey(char[] password) { - return privateKey; - } - }; - } -} diff --git a/graylog2-server/src/test/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedFileStorageTest.java b/graylog2-server/src/test/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedFileStorageTest.java deleted file mode 100644 index fd74727e65..0000000000 --- a/graylog2-server/src/test/java/org/graylog/security/certutil/privatekey/PrivateKeyEncryptedFileStorageTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.privatekey; - -import org.bouncycastle.pkcs.PKCSException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.file.Path; -import java.security.KeyPairGenerator; -import java.security.PrivateKey; - -import static org.graylog.security.certutil.CertConstants.KEY_GENERATION_ALGORITHM; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class PrivateKeyEncryptedFileStorageTest { - - @Test - void testKeyStorageSaveAndRetrieve(@TempDir Path tmpDir) throws Exception { - PrivateKeyEncryptedFileStorage privateKeyEncryptedFileStorage = new PrivateKeyEncryptedFileStorage(tmpDir.resolve("temp.key")); - char[] passwd = "password".toCharArray(); - - KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); - java.security.KeyPair certKeyPair = keyGen.generateKeyPair(); - final PrivateKey privateKey = certKeyPair.getPrivate(); - - privateKeyEncryptedFileStorage.writeEncryptedKey(passwd, privateKey); - final PrivateKey readPrivateKey = privateKeyEncryptedFileStorage.readEncryptedKey(passwd); - - assertEquals(privateKey, readPrivateKey); - } - - @Test - void testKeyStorageThrowsExceptionWhenUsingWrongPasswordDuringRead(@TempDir Path tmpDir) throws Exception { - PrivateKeyEncryptedFileStorage privateKeyEncryptedFileStorage = new PrivateKeyEncryptedFileStorage(tmpDir.resolve("temp.key")); - char[] passwd = "password".toCharArray(); - - KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); - java.security.KeyPair certKeyPair = keyGen.generateKeyPair(); - final PrivateKey privateKey = certKeyPair.getPrivate(); - - privateKeyEncryptedFileStorage.writeEncryptedKey(passwd, privateKey); - assertThrows(PKCSException.class, () -> privateKeyEncryptedFileStorage.readEncryptedKey("wrong password".toCharArray())); - } -}