Skip to content

Commit

Permalink
Security Module Plugin API (hyperledger#713)
Browse files Browse the repository at this point in the history
Introduced Security Module Plugin API. This allows to switch to a different security module provider to provide cryptographic function that can be used by NodeKey (such as sign, ECDHKeyAgreement etc.). By default register KeyPairSecurityModule otherwise attempt to load Security Module via plugin API.

CLI Options:
--security-module=<name>. (defaults to localfile)

Signed-off-by: Usman Saleem <usman@usmans.info>
  • Loading branch information
usmansaleem authored Apr 27, 2020
1 parent 136aa53 commit c2dcf62
Show file tree
Hide file tree
Showing 25 changed files with 646 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import org.hyperledger.besu.controller.BesuController;
import org.hyperledger.besu.controller.BesuControllerBuilder;
import org.hyperledger.besu.controller.GasLimitCalculator;
import org.hyperledger.besu.crypto.KeyPairSecurityModule;
import org.hyperledger.besu.crypto.KeyPairUtil;
import org.hyperledger.besu.crypto.NodeKey;
import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration;
import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
Expand All @@ -37,12 +39,14 @@
import org.hyperledger.besu.plugin.services.BesuConfiguration;
import org.hyperledger.besu.plugin.services.BesuEvents;
import org.hyperledger.besu.plugin.services.PicoCLIOptions;
import org.hyperledger.besu.plugin.services.SecurityModuleService;
import org.hyperledger.besu.plugin.services.StorageService;
import org.hyperledger.besu.plugin.services.storage.rocksdb.RocksDBPlugin;
import org.hyperledger.besu.services.BesuConfigurationImpl;
import org.hyperledger.besu.services.BesuEventsImpl;
import org.hyperledger.besu.services.BesuPluginContextImpl;
import org.hyperledger.besu.services.PicoCLIOptionsImpl;
import org.hyperledger.besu.services.SecurityModuleServiceImpl;
import org.hyperledger.besu.services.StorageServiceImpl;

import java.io.File;
Expand Down Expand Up @@ -71,10 +75,12 @@ public class ThreadBesuNodeRunner implements BesuNodeRunner {
private BesuPluginContextImpl buildPluginContext(
final BesuNode node,
final StorageServiceImpl storageService,
final SecurityModuleServiceImpl securityModuleService,
final BesuConfiguration commonPluginConfiguration) {
final CommandLine commandLine = new CommandLine(CommandSpec.create());
final BesuPluginContextImpl besuPluginContext = new BesuPluginContextImpl();
besuPluginContext.addService(StorageService.class, storageService);
besuPluginContext.addService(SecurityModuleService.class, securityModuleService);
besuPluginContext.addService(PicoCLIOptions.class, new PicoCLIOptionsImpl(commandLine));

final Path pluginsPath = node.homeDirectory().resolve("plugins");
Expand Down Expand Up @@ -105,12 +111,16 @@ public void startNode(final BesuNode node) {
ThreadContext.put("node", node.getName());

final StorageServiceImpl storageService = new StorageServiceImpl();
final SecurityModuleServiceImpl securityModuleService = new SecurityModuleServiceImpl();
final Path dataDir = node.homeDirectory();
final BesuConfiguration commonPluginConfiguration =
new BesuConfigurationImpl(dataDir, dataDir.resolve(DATABASE_PATH));
final BesuPluginContextImpl besuPluginContext =
besuPluginContextMap.computeIfAbsent(
node, n -> buildPluginContext(node, storageService, commonPluginConfiguration));
node,
n ->
buildPluginContext(
node, storageService, securityModuleService, commonPluginConfiguration));

final ObservableMetricsSystem metricsSystem =
PrometheusMetricsSystem.init(node.getMetricsConfiguration());
Expand Down Expand Up @@ -139,7 +149,7 @@ public void startNode(final BesuNode node) {
.dataDirectory(node.homeDirectory())
.miningParameters(node.getMiningParameters())
.privacyParameters(node.getPrivacyParameters())
.nodePrivateKeyFile(KeyPairUtil.getDefaultKeyFile(node.homeDirectory()))
.nodeKey(new NodeKey(new KeyPairSecurityModule(KeyPairUtil.loadKeyPair(dataDir))))
.metricsSystem(metricsSystem)
.transactionPoolConfiguration(TransactionPoolConfiguration.builder().build())
.ethProtocolConfiguration(EthProtocolConfiguration.defaultConfig())
Expand Down
68 changes: 51 additions & 17 deletions besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
import org.hyperledger.besu.cli.presynctasks.PrivateDatabaseMigrationPreSyncTask;
import org.hyperledger.besu.cli.subcommands.PasswordSubCommand;
import org.hyperledger.besu.cli.subcommands.PublicKeySubCommand;
import org.hyperledger.besu.cli.subcommands.PublicKeySubCommand.KeyLoader;
import org.hyperledger.besu.cli.subcommands.RetestethSubCommand;
import org.hyperledger.besu.cli.subcommands.blocks.BlocksSubCommand;
import org.hyperledger.besu.cli.subcommands.blocks.BlocksSubCommand.JsonBlockImporterFactory;
Expand All @@ -66,7 +65,9 @@
import org.hyperledger.besu.config.experimental.ExperimentalEIPs;
import org.hyperledger.besu.controller.BesuController;
import org.hyperledger.besu.controller.BesuControllerBuilder;
import org.hyperledger.besu.crypto.KeyPairSecurityModule;
import org.hyperledger.besu.crypto.KeyPairUtil;
import org.hyperledger.besu.crypto.NodeKey;
import org.hyperledger.besu.crypto.SECP256K1;
import org.hyperledger.besu.enclave.EnclaveFactory;
import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration;
Expand Down Expand Up @@ -110,16 +111,19 @@
import org.hyperledger.besu.plugin.services.BesuEvents;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.PicoCLIOptions;
import org.hyperledger.besu.plugin.services.SecurityModuleService;
import org.hyperledger.besu.plugin.services.StorageService;
import org.hyperledger.besu.plugin.services.exception.StorageException;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import org.hyperledger.besu.plugin.services.metrics.MetricCategoryRegistry;
import org.hyperledger.besu.plugin.services.securitymodule.SecurityModule;
import org.hyperledger.besu.plugin.services.storage.PrivacyKeyValueStorageFactory;
import org.hyperledger.besu.plugin.services.storage.rocksdb.RocksDBPlugin;
import org.hyperledger.besu.services.BesuConfigurationImpl;
import org.hyperledger.besu.services.BesuEventsImpl;
import org.hyperledger.besu.services.BesuPluginContextImpl;
import org.hyperledger.besu.services.PicoCLIOptionsImpl;
import org.hyperledger.besu.services.SecurityModuleServiceImpl;
import org.hyperledger.besu.services.StorageServiceImpl;
import org.hyperledger.besu.util.NetworkUtility;
import org.hyperledger.besu.util.PermissioningConfigurationValidator;
Expand Down Expand Up @@ -201,15 +205,12 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private final BesuController.Builder controllerBuilderFactory;
private final BesuPluginContextImpl besuPluginContext;
private final StorageServiceImpl storageService;
private final SecurityModuleServiceImpl securityModuleService;
private final Map<String, String> environment;
private final MetricCategoryRegistryImpl metricCategoryRegistry =
new MetricCategoryRegistryImpl();
private final MetricCategoryConverter metricCategoryConverter = new MetricCategoryConverter();

protected KeyLoader getKeyLoader() {
return KeyPairUtil::loadKeyPair;
}

// Public IP stored to prevent having to research it each time we need it.
private InetAddress autoDiscoveredDefaultIP = null;

Expand Down Expand Up @@ -815,6 +816,14 @@ void setBannedNodeIds(final List<String> values) {
arity = "1")
private String keyValueStorageName = DEFAULT_KEY_VALUE_STORAGE_NAME;

@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"})
@Option(
names = {"--security-module"},
paramLabel = "<NAME>",
description = "Identity for the Security Module to be used.",
arity = "1")
private String securityModuleName = DEFAULT_SECURITY_MODULE;

@Option(
names = {"--auto-log-bloom-caching-enabled"},
description = "Enable automatic log bloom caching (default: ${DEFAULT-VALUE})",
Expand Down Expand Up @@ -900,7 +909,8 @@ public BesuCommand(
controllerBuilderFactory,
besuPluginContext,
environment,
new StorageServiceImpl());
new StorageServiceImpl(),
new SecurityModuleServiceImpl());
}

@VisibleForTesting
Expand All @@ -913,7 +923,8 @@ protected BesuCommand(
final BesuController.Builder controllerBuilderFactory,
final BesuPluginContextImpl besuPluginContext,
final Map<String, String> environment,
final StorageServiceImpl storageService) {
final StorageServiceImpl storageService,
final SecurityModuleServiceImpl securityModuleService) {
this.logger = logger;
this.rlpBlockImporter = rlpBlockImporter;
this.rlpBlockExporterFactory = rlpBlockExporterFactory;
Expand All @@ -923,6 +934,7 @@ protected BesuCommand(
this.besuPluginContext = besuPluginContext;
this.environment = environment;
this.storageService = storageService;
this.securityModuleService = securityModuleService;
}

public void parse(
Expand Down Expand Up @@ -1000,7 +1012,8 @@ private BesuCommand addSubCommands(
resultHandler.out()));
commandLine.addSubcommand(
PublicKeySubCommand.COMMAND_NAME,
new PublicKeySubCommand(resultHandler.out(), getKeyLoader()));
new PublicKeySubCommand(
resultHandler.out(), this::addConfigurationService, this::buildNodeKey));
commandLine.addSubcommand(
PasswordSubCommand.COMMAND_NAME, new PasswordSubCommand(resultHandler.out()));
commandLine.addSubcommand(RetestethSubCommand.COMMAND_NAME, new RetestethSubCommand());
Expand Down Expand Up @@ -1046,6 +1059,7 @@ private BesuCommand handleUnstableOptions() {

private BesuCommand preparePlugins() {
besuPluginContext.addService(PicoCLIOptions.class, new PicoCLIOptionsImpl(commandLine));
besuPluginContext.addService(SecurityModuleService.class, securityModuleService);
besuPluginContext.addService(StorageService.class, storageService);
besuPluginContext.addService(MetricCategoryRegistry.class, metricCategoryRegistry);

Expand All @@ -1058,9 +1072,22 @@ private BesuCommand preparePlugins() {
.getMetricCategories()
.forEach(metricCategoryConverter::addRegistryCategory);

// register default security module
securityModuleService.register(
DEFAULT_SECURITY_MODULE, Suppliers.memoize(this::defaultSecurityModule)::get);

return this;
}

private SecurityModule defaultSecurityModule() {
return new KeyPairSecurityModule(loadKeyPair());
}

@VisibleForTesting
SECP256K1.KeyPair loadKeyPair() {
return KeyPairUtil.loadKeyPair(nodePrivateKeyFile());
}

private void parse(
final AbstractParseResultHandler<List<Object>> resultHandler,
final BesuExceptionHandler exceptionHandler,
Expand Down Expand Up @@ -1273,7 +1300,7 @@ public BesuControllerBuilder<?> getControllerBuilder() {
stratumExtranonce,
Optional.empty()))
.transactionPoolConfiguration(buildTransactionPoolConfiguration())
.nodePrivateKeyFile(nodePrivateKeyFile())
.nodeKey(buildNodeKey())
.metricsSystem(metricsSystem.get())
.privacyParameters(privacyParameters())
.clock(Clock.systemUTC())
Expand Down Expand Up @@ -1970,15 +1997,22 @@ private Path pluginsDir() {
}
}

public File nodePrivateKeyFile() {
File nodePrivateKeyFile = null;
if (isFullInstantiation()) {
nodePrivateKeyFile = standaloneCommands.nodePrivateKeyFile;
}
@VisibleForTesting
NodeKey buildNodeKey() {
return new NodeKey(securityModule());
}

private SecurityModule securityModule() {
return securityModuleService
.getByName(securityModuleName)
.orElseThrow(() -> new RuntimeException("Security Module not found: " + securityModuleName))
.get();
}

return nodePrivateKeyFile != null
? nodePrivateKeyFile
: KeyPairUtil.getDefaultKeyFile(dataDir());
private File nodePrivateKeyFile() {
final Optional<File> nodePrivateKeyFile =
isDocker ? Optional.empty() : Optional.ofNullable(standaloneCommands.nodePrivateKeyFile);
return nodePrivateKeyFile.orElseGet(() -> KeyPairUtil.getDefaultKeyFile(dataDir()));
}

private File privacyPublicKeyFile() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public interface DefaultCommandValues {
float DEFAULT_FRACTION_REMOTE_WIRE_CONNECTIONS_ALLOWED =
RlpxConfiguration.DEFAULT_FRACTION_REMOTE_CONNECTIONS_ALLOWED;
String DEFAULT_KEY_VALUE_STORAGE_NAME = "rocksdb";
String DEFAULT_SECURITY_MODULE = "localfile";

static Path getDefaultBesuDataPath(final Object command) {
// this property is retrieved from Gradle tasks or Besu running shell script.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.hyperledger.besu.cli.DefaultCommandValues;
import org.hyperledger.besu.cli.subcommands.PublicKeySubCommand.AddressSubCommand;
import org.hyperledger.besu.cli.subcommands.PublicKeySubCommand.ExportSubCommand;
import org.hyperledger.besu.crypto.SECP256K1.KeyPair;
import org.hyperledger.besu.crypto.NodeKey;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Util;

Expand All @@ -33,6 +33,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -62,25 +63,29 @@ public class PublicKeySubCommand implements Runnable {
private CommandSpec spec; // Picocli injects reference to command spec

private final PrintStream out;
private final KeyLoader keyLoader;
private final Runnable besuConfigurationService;
private final Supplier<NodeKey> nodeKey;

public PublicKeySubCommand(final PrintStream out, final KeyLoader keyLoader) {
public PublicKeySubCommand(
final PrintStream out,
final Runnable besuConfigurationService,
final Supplier<NodeKey> nodeKey) {
this.out = out;
this.keyLoader = keyLoader;
this.besuConfigurationService = besuConfigurationService;
this.nodeKey = nodeKey;
}

@Override
public void run() {
spec.commandLine().usage(out);
}

private Optional<KeyPair> getKeyPair() {
try {
return Optional.of(keyLoader.load(parentCommand.nodePrivateKeyFile()));
} catch (IOException e) {
LOG.error("An error occurred while trying to read the private key", e);
return Optional.empty();
}
private void initBesuConfigurationService() {
besuConfigurationService.run();
}

private NodeKey getNodeKey() {
return nodeKey.get();
}

/**
Expand Down Expand Up @@ -113,22 +118,24 @@ public void run() {
checkNotNull(parentCommand);
checkNotNull(parentCommand.parentCommand);

parentCommand.getKeyPair().ifPresent(this::outputPublicKey);
parentCommand.initBesuConfigurationService();
final NodeKey nodeKey = parentCommand.getNodeKey();
Optional.ofNullable(nodeKey).ifPresent(this::outputPublicKey);
}

private void outputPublicKey(final KeyPair keyPair) {
private void outputPublicKey(final NodeKey nodeKey) {
// if we have an output file defined, print to it
// otherwise print to standard output.
if (publicKeyExportFile != null) {
final Path path = publicKeyExportFile.toPath();

try (final BufferedWriter fileWriter = Files.newBufferedWriter(path, UTF_8)) {
fileWriter.write(keyPair.getPublicKey().toString());
fileWriter.write(nodeKey.getPublicKey().toString());
} catch (final IOException e) {
LOG.error("An error occurred while trying to write the public key", e);
}
} else {
parentCommand.out.println(keyPair.getPublicKey().toString());
parentCommand.out.println(nodeKey.getPublicKey().toString());
}
}
}
Expand Down Expand Up @@ -165,11 +172,13 @@ public void run() {
checkNotNull(parentCommand);
checkNotNull(parentCommand.parentCommand);

parentCommand.getKeyPair().ifPresent(this::outputAddress);
parentCommand.initBesuConfigurationService();
final NodeKey nodeKey = parentCommand.getNodeKey();
Optional.ofNullable(nodeKey).ifPresent(this::outputAddress);
}

private void outputAddress(final KeyPair keyPair) {
final Address address = Util.publicKeyToAddress(keyPair.getPublicKey());
private void outputAddress(final NodeKey nodeKey) {
final Address address = Util.publicKeyToAddress(nodeKey.getPublicKey());

// if we have an output file defined, print to it
// otherwise print to standard output.
Expand All @@ -186,10 +195,4 @@ private void outputAddress(final KeyPair keyPair) {
}
}
}

@FunctionalInterface
public interface KeyLoader {

KeyPair load(final File keyFile) throws IOException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@
package org.hyperledger.besu.controller;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.hyperledger.besu.crypto.KeyPairUtil.loadKeyPair;

import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.experimental.ExperimentalEIPs;
import org.hyperledger.besu.crypto.BouncyCastleSecurityModule;
import org.hyperledger.besu.crypto.NodeKey;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethods;
Expand Down Expand Up @@ -61,7 +59,6 @@
import org.hyperledger.besu.metrics.ObservableMetricsSystem;

import java.io.Closeable;
import java.io.File;
import java.math.BigInteger;
import java.nio.file.Path;
import java.time.Clock;
Expand Down Expand Up @@ -129,11 +126,6 @@ public BesuControllerBuilder<C> miningParameters(final MiningParameters miningPa
return this;
}

public BesuControllerBuilder<C> nodePrivateKeyFile(final File nodePrivateKeyFile) {
this.nodeKey = new NodeKey(new BouncyCastleSecurityModule(loadKeyPair(nodePrivateKeyFile)));
return this;
}

public BesuControllerBuilder<C> nodeKey(final NodeKey nodeKey) {
this.nodeKey = nodeKey;
return this;
Expand Down
Loading

0 comments on commit c2dcf62

Please sign in to comment.