diff --git a/data-node/src/main/java/org/graylog/datanode/bindings/ServerBindings.java b/data-node/src/main/java/org/graylog/datanode/bindings/ServerBindings.java index 77bea84831..558cd0d9fe 100644 --- a/data-node/src/main/java/org/graylog/datanode/bindings/ServerBindings.java +++ b/data-node/src/main/java/org/graylog/datanode/bindings/ServerBindings.java @@ -16,12 +16,13 @@ */ package org.graylog.datanode.bindings; -import com.google.inject.Scopes; import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.OptionalBinder; import org.graylog.datanode.Configuration; import org.graylog.datanode.cluster.DataNodeServiceImpl; import org.graylog.datanode.shared.system.activities.DataNodeActivityWriter; +import org.graylog.security.certutil.keystore.storage.KeystoreContentMover; +import org.graylog.security.certutil.keystore.storage.PrivateKeyEntryOnlyKeystoreContentMover; import org.graylog2.bindings.providers.ClusterEventBusProvider; import org.graylog2.cluster.ClusterConfigServiceImpl; import org.graylog2.cluster.NodeService; @@ -74,6 +75,7 @@ public class ServerBindings extends Graylog2Module { private void bindSingletons() { bind(ClusterConfigService.class).to(ClusterConfigServiceImpl.class).asEagerSingleton(); + bind(KeystoreContentMover.class).to(PrivateKeyEntryOnlyKeystoreContentMover.class).asEagerSingleton(); } private void bindInterfaces() { diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java index 1534276417..4f5b9b665b 100644 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java @@ -120,7 +120,7 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { ); final KeystoreMongoLocation location = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.DATA_NODE_KEYSTORE_COLLECTION); - keystoreStorage.writeKeyStore(location, nodeKeystore, configuration.getDatanodeHttpCertificatePassword().toCharArray()); + keystoreStorage.writeKeyStore(location, nodeKeystore, configuration.getDatanodeHttpCertificatePassword().toCharArray(), null); //should be in one transaction, but we miss transactions... nodePreflightConfigService.changeState(nodeId.getNodeId(), NodePreflightConfig.State.STORED); diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/certificates/KeystoreReEncryption.java b/data-node/src/main/java/org/graylog/datanode/configuration/certificates/KeystoreReEncryption.java new file mode 100644 index 0000000000..fb710d50f2 --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/configuration/certificates/KeystoreReEncryption.java @@ -0,0 +1,72 @@ +/* + * 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.certificates; + +import org.apache.commons.lang3.RandomStringUtils; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.keystore.storage.SmartKeystoreStorage; +import org.graylog.security.certutil.keystore.storage.location.KeystoreLocation; + +import javax.inject.Inject; +import javax.inject.Named; +import java.security.KeyStore; +import java.util.Optional; + +public class KeystoreReEncryption { + + private final SmartKeystoreStorage keystoreStorage; + private final String passwordSecret; + + @Inject + public KeystoreReEncryption(final SmartKeystoreStorage keystoreStorage, + final @Named("password_secret") String passwordSecret) { + this.keystoreStorage = keystoreStorage; + this.passwordSecret = passwordSecret; + } + + public void reEncyptWithSecret(final KeystoreLocation originalLocation, + final char[] originalPassword, + final KeystoreLocation targetLocation + ) throws KeyStoreStorageException { + reEncypt(originalLocation, originalPassword, targetLocation, passwordSecret.toCharArray()); + } + + public char[] reEncyptWithOtp(final KeystoreLocation originalLocation, + final char[] originalPassword, + final KeystoreLocation targetLocation + ) throws KeyStoreStorageException { + final char[] oneTimePassword = RandomStringUtils.randomAlphabetic(256).toCharArray(); + reEncypt(originalLocation, originalPassword, targetLocation, oneTimePassword); + return oneTimePassword; + } + + private void reEncypt(final KeystoreLocation originalLocation, + final char[] originalPassword, + final KeystoreLocation targetLocation, + final char[] newPassword + ) throws KeyStoreStorageException { + + final Optional keyStore = keystoreStorage.readKeyStore(originalLocation, originalPassword); + if (keyStore.isPresent()) { + final KeyStore originalKeystore = keyStore.get(); + keystoreStorage.writeKeyStore(targetLocation, originalKeystore, originalPassword, newPassword); + + } else { + throw new KeyStoreStorageException("No keystore present in : " + originalLocation); + } + } +} 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 bd2083ef75..a4b2d183a0 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 @@ -24,7 +24,9 @@ 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.security.certutil.keystore.storage.KeystoreContentMover; import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage; +import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -47,6 +49,7 @@ import static org.graylog.security.certutil.CertConstants.KEY_GENERATION_ALGORIT import static org.graylog.security.certutil.CertConstants.SIGNING_ALGORITHM; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; public class TruststoreCreatorTest { @@ -73,8 +76,8 @@ public class TruststoreCreatorTest { assertTrue(trustStoreFile.toFile().exists()); - KeystoreFileStorage keystoreFileStorage = new KeystoreFileStorage(); - final Optional keyStoreOptional = keystoreFileStorage.readKeyStore(trustStoreFile, truststorePassword); + KeystoreFileStorage keystoreFileStorage = new KeystoreFileStorage(mock(KeystoreContentMover.class)); + final Optional keyStoreOptional = keystoreFileStorage.readKeyStore(new KeystoreFileLocation(trustStoreFile), truststorePassword); assertTrue(keyStoreOptional.isPresent()); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCa.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCa.java index bceca82afb..5c0bdd10d2 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCa.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCa.java @@ -22,6 +22,7 @@ import org.graylog.security.certutil.ca.CACreator; import org.graylog.security.certutil.console.CommandLineConsole; import org.graylog.security.certutil.console.SystemConsole; import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage; +import org.graylog.security.certutil.keystore.storage.PrivateKeyEntryOnlyKeystoreContentMover; import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; import org.graylog2.bootstrap.CliCommand; @@ -41,14 +42,14 @@ public class CertutilCa implements CliCommand { public CertutilCa() { this.console = new SystemConsole(); this.caCreator = new CACreator(); - this.caKeystoreStorage = new KeystoreFileStorage(); + this.caKeystoreStorage = new KeystoreFileStorage(new PrivateKeyEntryOnlyKeystoreContentMover()); } public CertutilCa(String keystoreFilename, CommandLineConsole console) { this.keystoreFilename = keystoreFilename; this.console = console; this.caCreator = new CACreator(); - this.caKeystoreStorage = new KeystoreFileStorage(); + this.caKeystoreStorage = new KeystoreFileStorage(new PrivateKeyEntryOnlyKeystoreContentMover()); } @Override @@ -65,8 +66,7 @@ public class CertutilCa implements CliCommand { console.printLine("Private keys and certificates for root and intermediate CA generated"); final Path keystorePath = Path.of(keystoreFilename); - //TODO: it is probably a bad idea to use the same password for CA and its storage... - caKeystoreStorage.writeKeyStore(new KeystoreFileLocation(keystorePath), caKeystore, password); + caKeystoreStorage.writeKeyStore(new KeystoreFileLocation(keystorePath), caKeystore, password, null); console.printLine("Keys and certificates stored in " + keystorePath.toAbsolutePath()); } catch (Exception e) { diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/ca/exceptions/KeyStoreStorageException.java b/graylog2-server/src/main/java/org/graylog/security/certutil/ca/exceptions/KeyStoreStorageException.java index 7c118b6ba4..281027cd76 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/ca/exceptions/KeyStoreStorageException.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/ca/exceptions/KeyStoreStorageException.java @@ -21,4 +21,8 @@ public class KeyStoreStorageException extends Exception { public KeyStoreStorageException(String message, Throwable cause) { super(message, cause); } + + public KeyStoreStorageException(String message) { + super(message); + } } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreContentMover.java b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreContentMover.java new file mode 100644 index 0000000000..a16124e0bb --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreContentMover.java @@ -0,0 +1,28 @@ +/* + * 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.keystore.storage; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +public interface KeystoreContentMover { + + KeyStore moveContents(final KeyStore originalKeyStore, + char[] currentPassword, + final char[] newPassword) throws GeneralSecurityException, IOException; +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java index bdce3256e4..b0b92d55a1 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorage.java @@ -30,13 +30,25 @@ import java.util.Optional; public final class KeystoreFileStorage implements KeystoreStorage { + private final KeystoreContentMover keystoreContentMover; + + public KeystoreFileStorage(final KeystoreContentMover keystoreContentMover) { + this.keystoreContentMover = keystoreContentMover; + } + @Override public void writeKeyStore(final KeystoreFileLocation location, final KeyStore keyStore, - final char[] password) throws KeyStoreStorageException { + final char[] currentPassword, + final char[] newPassword) throws KeyStoreStorageException { final Path keystorePath = location.keystorePath(); try (FileOutputStream store = new FileOutputStream(keystorePath.toFile())) { - keyStore.store(store, password); + if (newPassword == null) { + keyStore.store(store, currentPassword); + } else { + KeyStore newKeyStore = keystoreContentMover.moveContents(keyStore, currentPassword, newPassword); + newKeyStore.store(store, newPassword); + } } catch (Exception ex) { throw new KeyStoreStorageException("Failed to save keystore to " + keystorePath, ex); } @@ -62,7 +74,7 @@ public final class KeystoreFileStorage implements KeystoreStorage { private final CertificatesService certificatesService; + private final KeystoreContentMover keystoreContentMover; @Inject - public KeystoreMongoStorage(final CertificatesService certificatesService) { + public KeystoreMongoStorage(final CertificatesService certificatesService, + final KeystoreContentMover keystoreContentMover) { this.certificatesService = certificatesService; + this.keystoreContentMover = keystoreContentMover; } @Override - public void writeKeyStore(KeystoreMongoLocation location, KeyStore keyStore, char[] password) throws KeyStoreStorageException { + public void writeKeyStore(KeystoreMongoLocation location, + KeyStore keyStore, + char[] currentPassword, + final char[] newPassword) throws KeyStoreStorageException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - keyStore.store(baos, password); + if (newPassword == null) { + keyStore.store(baos, currentPassword); + } else { + KeyStore newKeyStore = keystoreContentMover.moveContents(keyStore, currentPassword, newPassword); + newKeyStore.store(baos, newPassword); + } final String keystoreDataAsString = Base64.getEncoder().encodeToString(baos.toByteArray()); certificatesService.writeCert(location, keystoreDataAsString); } catch (Exception ex) { diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreStorage.java index 6fac54c1c2..9a47b357c3 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/KeystoreStorage.java @@ -26,8 +26,9 @@ public sealed interface KeystoreStorage permits Keys void writeKeyStore(final T location, final KeyStore keyStore, - final char[] password) - throws KeyStoreStorageException; + final char[] currentPassword, + final char[] newPassword + ) throws KeyStoreStorageException; Optional readKeyStore(final T location, char[] password) throws KeyStoreStorageException; diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/PrivateKeyEntryOnlyKeystoreContentMover.java b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/PrivateKeyEntryOnlyKeystoreContentMover.java new file mode 100644 index 0000000000..2c12ce58f8 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/PrivateKeyEntryOnlyKeystoreContentMover.java @@ -0,0 +1,50 @@ +/* + * 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.keystore.storage; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Enumeration; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + +//TODO: temporary and limited implementation... +//Supporting all the possibilities would be a nightmare +//The goal is to introduce a new class for KeyStore, which supports only one type of password across all entries, one entry type... +public class PrivateKeyEntryOnlyKeystoreContentMover implements KeystoreContentMover { + + @Override + public KeyStore moveContents(KeyStore originalKeyStore, + char[] currentPassword, + final char[] newPassword) throws GeneralSecurityException, IOException { + KeyStore newKeyStore = KeyStore.getInstance(PKCS12); + newKeyStore.load(null, newPassword); + + final Enumeration aliases = originalKeyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + newKeyStore.setKeyEntry( + alias, + originalKeyStore.getKey(alias, currentPassword), + newPassword, + originalKeyStore.getCertificateChain(alias) + ); + } + return newKeyStore; + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/SmartKeystoreStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/SmartKeystoreStorage.java index 63975c127b..93752461f3 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/SmartKeystoreStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/keystore/storage/SmartKeystoreStorage.java @@ -26,26 +26,27 @@ import javax.inject.Inject; import java.security.KeyStore; import java.util.Optional; - public final class SmartKeystoreStorage implements KeystoreStorage { private final KeystoreMongoStorage keystoreMongoStorage; private final KeystoreFileStorage keystoreFileStorage; @Inject - public SmartKeystoreStorage(final CertificatesService certificatesService) { - keystoreMongoStorage = new KeystoreMongoStorage(certificatesService); - keystoreFileStorage = new KeystoreFileStorage(); + public SmartKeystoreStorage(final CertificatesService certificatesService, + final KeystoreContentMover keystoreContentMover) { + keystoreMongoStorage = new KeystoreMongoStorage(certificatesService, keystoreContentMover); + keystoreFileStorage = new KeystoreFileStorage(keystoreContentMover); } @Override public void writeKeyStore(KeystoreLocation location, KeyStore keyStore, - char[] password) throws KeyStoreStorageException { + char[] currentPassword, + final char[] newPassword) throws KeyStoreStorageException { if (location instanceof final KeystoreMongoLocation mongoLocation) { - keystoreMongoStorage.writeKeyStore(mongoLocation, keyStore, password); + keystoreMongoStorage.writeKeyStore(mongoLocation, keyStore, currentPassword, newPassword); } else if (location instanceof final KeystoreFileLocation fileLocation) { - keystoreFileStorage.writeKeyStore(fileLocation, keyStore, password); + keystoreFileStorage.writeKeyStore(fileLocation, keyStore, currentPassword, newPassword); } } diff --git a/graylog2-server/src/test/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorageTest.java b/graylog2-server/src/test/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorageTest.java index 5dbc23ea32..6b0da25d64 100644 --- a/graylog2-server/src/test/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorageTest.java +++ b/graylog2-server/src/test/java/org/graylog/security/certutil/keystore/storage/KeystoreFileStorageTest.java @@ -18,44 +18,70 @@ package org.graylog.security.certutil.keystore.storage; import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; -import org.junit.jupiter.api.BeforeEach; +import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.FileInputStream; import java.nio.file.Path; import java.security.KeyStore; +import java.security.UnrecoverableKeyException; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; public class KeystoreFileStorageTest { private KeystoreFileStorage toTest; - @BeforeEach - void setUp() { - toTest = new KeystoreFileStorage(); - } - @Test - void testKeyStoreSaveAndRetrieve(@TempDir Path tmpDir) throws Exception { + void testKeyStoreSaveAndRetrieveWithNoPasswordChange(@TempDir Path tmpDir) throws Exception { + final KeystoreContentMover keystoreContentMover = mock(KeystoreContentMover.class); + toTest = new KeystoreFileStorage(keystoreContentMover); final Path keystoreFile = tmpDir.resolve("keystore_file.p12"); KeyStore testKeystore = KeyStore.getInstance(CertConstants.PKCS12); testKeystore.load(new FileInputStream("src/test/resources/org/graylog/security/certutil/keystore/storage/sample_certificate_keystore.p12"), "password".toCharArray()); char[] passwd = "password".toCharArray(); - toTest.writeKeyStore(keystoreFile, testKeystore, passwd); - final Optional keyStore = toTest.readKeyStore(keystoreFile, passwd); + final KeystoreFileLocation location = new KeystoreFileLocation(keystoreFile); + toTest.writeKeyStore(location, testKeystore, passwd, null); + final Optional keyStore = toTest.readKeyStore(location, passwd); assertTrue(keyStore.isPresent()); - assertEquals(testKeystore.getCertificate("datanode"), keyStore.get().getCertificate("datanode")); + final KeyStore keyStoreToVerify = keyStore.get(); + assertEquals(testKeystore.getCertificate("datanode"), keyStoreToVerify.getCertificate("datanode")); + assertEquals("RSA", keyStoreToVerify.getKey("datanode", passwd).getAlgorithm()); + verifyNoInteractions(keystoreContentMover); + + } + + @Test + void testKeyStoreSaveAndRetrieveWithPasswordChange(@TempDir Path tmpDir) throws Exception { + toTest = new KeystoreFileStorage(new PrivateKeyEntryOnlyKeystoreContentMover()); + final Path keystoreFile = tmpDir.resolve("keystore_file.p12"); + KeyStore testKeystore = KeyStore.getInstance(CertConstants.PKCS12); + testKeystore.load(new FileInputStream("src/test/resources/org/graylog/security/certutil/keystore/storage/sample_certificate_keystore.p12"), "password".toCharArray()); + char[] passwd = "password".toCharArray(); + char[] newPasswd = "secret".toCharArray(); + + final KeystoreFileLocation location = new KeystoreFileLocation(keystoreFile); + toTest.writeKeyStore(location, testKeystore, passwd, newPasswd); + final Optional keyStore = toTest.readKeyStore(location, newPasswd); + assertTrue(keyStore.isPresent()); + final KeyStore keyStoreToVerify = keyStore.get(); + assertEquals(testKeystore.getCertificate("datanode"), keyStoreToVerify.getCertificate("datanode")); + assertEquals("RSA", keyStoreToVerify.getKey("datanode", newPasswd).getAlgorithm()); + assertThrows(UnrecoverableKeyException.class, () -> keyStoreToVerify.getKey("datanode", passwd)); } @Test void testKeystoreReadThrowsExceptionWhenUsingWrongPassword(@TempDir Path tmpDir) throws Exception { + final KeystoreContentMover keystoreContentMover = mock(KeystoreContentMover.class); + toTest = new KeystoreFileStorage(keystoreContentMover); final Path keystoreFile = tmpDir.resolve("keystore_file.p12"); KeyStore testKeystore = KeyStore.getInstance(CertConstants.PKCS12); testKeystore.load(null, null); @@ -63,6 +89,7 @@ public class KeystoreFileStorageTest { toTest.writeKeyStore(keystoreFile, testKeystore, passwd); assertThrows(KeyStoreStorageException.class, () -> toTest.readKeyStore(keystoreFile, "wrong password".toCharArray())); + verifyNoInteractions(keystoreContentMover); } }