Skip to content

Commit

Permalink
feat: Pkcs11 Security Module Service implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
usmansaleem committed Jul 8, 2024
1 parent 927000b commit abe3e12
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 22 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Besu Plugin - PKCS11 SoftHSM

A [Besu plugin](https://besu.hyperledger.org/private-networks/reference/plugin-api-interfaces) that shows how to
integrate with HSM with PKCS11 interface. SoftHSM is used as a test HSM.
A [Besu plugin][1] that provides a custom security module to load the [node key][2] from an HSM, such as [SoftHSM][3],
using PKCS11 libraries.

[1]: <https://besu.hyperledger.org/private-networks/reference/plugin-api-interfaces>
[2]: <https://besu.hyperledger.org/public-networks/concepts/node-keys>
[3]: <https://www.opendnssec.org/softhsm/>

![GitHub Actions Workflow Status](https://github.com/usmansaleem/besu-pkcs11-plugin/actions/workflows/ci.yml/badge.svg?branch=main)
![GitHub Release](https://img.shields.io/github/v/release/usmansaleem/besu-pkcs11-plugin?include_prereleases)
Expand Down Expand Up @@ -31,8 +35,8 @@ The plugin jar will be available at `build/libs/besu-pkcs11-plugin-<version>.jar

## Usage

Drop the `jar` in the `/plugins` folder under Besu installation. This plugin will expose following additional cli
options:
Drop the `besu-pkcs11-plugin-<version>.jar` in the `/plugins` folder under Besu installation. This plugin will expose
following additional cli options:
`TBA`

## Linux SoftHSM Setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,42 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A Besu plugin that provides a custom security module to load the node key from an HSM using
* PKCS11 libraries.
*/
@AutoService(BesuPlugin.class)
public class BesuPkcs11SoftHsmPlugin implements BesuPlugin {
static final String SECURITY_MODULE_NAME = "pkcs11-softhsm";
private static final Logger LOG = LoggerFactory.getLogger(BesuPkcs11SoftHsmPlugin.class);
public class Pkcs11HsmPlugin implements BesuPlugin {
static final String SECURITY_MODULE_NAME = "pkcs11-hsm";
private static final Logger LOG = LoggerFactory.getLogger(Pkcs11HsmPlugin.class);
private final Pkcs11PluginCliOptions cliParams = new Pkcs11PluginCliOptions();

@Override
public void register(final BesuContext besuContext) {
LOG.debug("Registering plugin ...");
LOG.info("Registering plugin ...");
registerCliOptions(besuContext);
registerSecurityModule(besuContext);
}

/**
* Registers {@code Pkcs11PluginCliOptions} with {@code PicoCLIOptions} service provided by {@code
* BesuContext}.
*
* @param besuContext An instance of {@code BesuContext}
*/
private void registerCliOptions(final BesuContext besuContext) {
besuContext
.getService(PicoCLIOptions.class)
.orElseThrow(() -> new IllegalStateException("Expecting PicoCLIOptions to be present"))
.addPicoCLIOptions(SECURITY_MODULE_NAME, cliParams);
}

/**
* Registers {@code Pkcs11SecurityModule} with the {@code SecurityModuleService} service provided
* by {@code BesuContext}.
*
* @param besuContext An instance of {@code BesuContext}
*/
private void registerSecurityModule(final BesuContext besuContext) {
// lazy-init our security module implementation during register phase
besuContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
package info.usmans.besu.plugin.softhsm;

import static info.usmans.besu.plugin.softhsm.BesuPkcs11SoftHsmPlugin.SECURITY_MODULE_NAME;
import static info.usmans.besu.plugin.softhsm.Pkcs11HsmPlugin.SECURITY_MODULE_NAME;

import java.nio.file.Path;
import picocli.CommandLine.Option;

/**
* Represents cli options that are required by the Besu PKCS11-SoftHSM plugin. Provides {@code
* --plugin-pkcs11-softhsm-config-path} option.
*/
/** Represents cli options that are required by the Besu PKCS11-SoftHSM plugin. */
public class Pkcs11PluginCliOptions {
@Option(
names = "--plugin-" + SECURITY_MODULE_NAME + "-config-path",
Expand All @@ -19,6 +16,20 @@ public class Pkcs11PluginCliOptions {
paramLabel = "<path>")
private Path pkcs11ConfigPath;

@Option(
names = "--plugin-" + SECURITY_MODULE_NAME + "-password-path",
description = "Path to the file that contains password or PIN to access PKCS11 token",
required = true,
paramLabel = "<path>")
private Path pkcs11PasswordPath;

@Option(
names = "--plugin-" + SECURITY_MODULE_NAME + "-key-alias",
description = "Alias or label of the private key that is stored in the HSM",
required = true,
paramLabel = "<path>")
private String privateKeyAlias;

/** Default constructor. Performs no initialization. */
public Pkcs11PluginCliOptions() {}

Expand All @@ -39,4 +50,22 @@ public Pkcs11PluginCliOptions(final Path pkcs11ConfigPath) {
public Path getPkcs11ConfigPath() {
return pkcs11ConfigPath;
}

/**
* Returns the path to the file that contains the password or PIN to access the PKCS11 token.
*
* @return the path to the file that contains the password or PIN to access the PKCS11 token
*/
public Path getPkcs11PasswordPath() {
return pkcs11PasswordPath;
}

/**
* Returns the alias or label of the private key that is stored in the HSM.
*
* @return the alias or label of the private key that is stored in the HSM
*/
public String getPrivateKeyAlias() {
return privateKeyAlias;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
package info.usmans.besu.plugin.softhsm;

import static info.usmans.besu.plugin.softhsm.SignatureUtil.extractRAndSFromDERSignature;

import java.io.IOException;
import java.nio.file.Files;
import java.security.Key;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import javax.crypto.KeyAgreement;
import org.apache.tuweni.bytes.Bytes32;
import org.hyperledger.besu.plugin.services.securitymodule.SecurityModule;
import org.hyperledger.besu.plugin.services.securitymodule.SecurityModuleException;
Expand All @@ -10,32 +23,162 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Security Module implementation that interacts with a HSM (such as SoftHSM) using PKCS11
* interface.
*/
/** A PKCS11 based implementation of Besu SecurityModule interface. */
public class Pkcs11SecurityModuleService implements SecurityModule {
private static final Logger LOG = LoggerFactory.getLogger(Pkcs11SecurityModuleService.class);
private final Pkcs11PluginCliOptions cliParams;
private Provider provider;
private KeyStore keyStore;
private PrivateKey privateKey;
private ECPublicKey ecPublicKey;
private ECParameterSpec secp256k1Param;

public Pkcs11SecurityModuleService(final Pkcs11PluginCliOptions cliParams) {
LOG.debug("Creating Pkcs11SecurityModuleService ...");
this.cliParams = cliParams;
// verify we have config file and able to access softhsm
validateCliOptions();
loadPkcs11Provider();
loadPkcs11Keystore();
loadPkcs11PrivateKey();
loadPkcs11PublicKey();
}

private void validateCliOptions() {
if (cliParams.getPkcs11ConfigPath() == null) {
throw new SecurityModuleException("PKCS11 configuration file path is not provided");
}
if (cliParams.getPkcs11PasswordPath() == null) {
throw new SecurityModuleException("PKCS11 password file path is not provided");
}
if (cliParams.getPrivateKeyAlias() == null) {
throw new SecurityModuleException("PKCS11 private key alias is not provided");
}
}

private void loadPkcs11Provider() {
// initialize PKCS11 provider
LOG.info("Initializing PKCS11 provider ...");

try {
provider =
Security.getProvider("SUNPKCS11").configure(cliParams.getPkcs11ConfigPath().toString());
Security.addProvider(provider);
} catch (final Exception e) {
throw new SecurityModuleException(
"Error encountered while loading SunPKCS11 provider with configuration: "
+ cliParams.getPkcs11ConfigPath().toString(),
e);
}
}

private void loadPkcs11Keystore() {
LOG.info("Loading PKCS11 keystore ...");
final char[] charArray;
try {
charArray = Files.readString(cliParams.getPkcs11PasswordPath()).toCharArray();
} catch (final IOException e) {
throw new SecurityModuleException(
"Error reading file: " + cliParams.getPkcs11PasswordPath(), e);
}

try {
keyStore = KeyStore.getInstance("PKCS11", provider);
keyStore.load(null, charArray);
} catch (final Exception e) {
throw new SecurityModuleException("Error loading PKCS11 keystore", e);
}
}

private void loadPkcs11PrivateKey() {
LOG.info("Loading private key ...");
final Key key;
try {
key = keyStore.getKey(cliParams.getPrivateKeyAlias(), new char[0]);
} catch (final Exception e) {
throw new SecurityModuleException(
"Error loading private key for alias: " + cliParams.getPrivateKeyAlias(), e);
}

if (!(key instanceof PrivateKey)) {
throw new SecurityModuleException(
"Loaded key is not a PrivateKey for alias: " + cliParams.getPrivateKeyAlias());
}

privateKey = (PrivateKey) key;
}

private void loadPkcs11PublicKey() {
LOG.info("Loading public key ...");
final Certificate certificate;
try {
certificate = keyStore.getCertificate(cliParams.getPrivateKeyAlias());
if (certificate == null) {
throw new SecurityModuleException(
"Certificate not found for private key alias: " + cliParams.getPrivateKeyAlias());
}
} catch (final Exception e) {
throw new SecurityModuleException(
"Error while loading certificate for private key alias: "
+ cliParams.getPrivateKeyAlias(),
e);
}

final java.security.PublicKey publicKey;
try {
publicKey = certificate.getPublicKey();
} catch (final Exception e) {
throw new SecurityModuleException(
"Error while loading public key for alias: " + cliParams.getPrivateKeyAlias(), e);
}

if (!(publicKey instanceof ECPublicKey)) {
throw new RuntimeException(
"Public Key is not a valid ECPublicKey for alias: " + cliParams.getPrivateKeyAlias());
}
ecPublicKey = (ECPublicKey) publicKey;
// we could use a constant, for now we will get it from the public key
secp256k1Param = ecPublicKey.getParams();
}

@Override
public Signature sign(Bytes32 dataHash) throws SecurityModuleException {
return null;
try {
// Java classes generate ASN1 encoded signature,
// Besu needs P1363 i.e. R and S of the signature
final java.security.Signature signature =
java.security.Signature.getInstance("SHA256WithECDSA", provider);
signature.initSign(privateKey);
signature.update(dataHash.toArray());
final byte[] sigBytes = signature.sign();
return extractRAndSFromDERSignature(sigBytes);
} catch (final Exception e) {
if (e instanceof SecurityModuleException) {
throw (SecurityModuleException) e;
}
throw new SecurityModuleException("Error initializing signature", e);
}
}

@Override
public PublicKey getPublicKey() throws SecurityModuleException {
return null;
return ecPublicKey::getW;
}

@Override
public Bytes32 calculateECDHKeyAgreement(PublicKey partyKey) throws SecurityModuleException {
return null;
public Bytes32 calculateECDHKeyAgreement(PublicKey theirKey) throws SecurityModuleException {
LOG.debug("Calculating ECDH key agreement ...");
// convert Besu PublicKey (which wraps ECPoint) to java.security.PublicKey
java.security.PublicKey theirPublicKey =
SignatureUtil.eCPointToPublicKey(theirKey.getW(), secp256k1Param, provider);

// generate ECDH Key Agreement
try {
final KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", provider);
keyAgreement.init(privateKey);
keyAgreement.doPhase(theirPublicKey, true);
return Bytes32.wrap(keyAgreement.generateSecret());
} catch (final Exception e) {
throw new SecurityModuleException("Error calculating ECDH key agreement", e);
}
}
}
Loading

0 comments on commit abe3e12

Please sign in to comment.