Skip to content

Commit

Permalink
feat: Add Dockerfile to test the plugin. (#10)
Browse files Browse the repository at this point in the history
* Dockerfile to test the plugin
* Use constant for SECP256K1 curve
* cleanup docs
  • Loading branch information
usmansaleem authored Jul 9, 2024
1 parent 9f8b4d5 commit f74b98d
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 28 deletions.
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright 2024, Usman Saleem.
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

# Exclude everything
*

# Include specific files and directories needed for the build
!docker/scripts/entrypoint.sh
!Dockerfile
!build/libs/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
build

.idea

# Ignore data and tokens in volume directory
docker/volumes/data
docker/volumes/tokens
44 changes: 44 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# syntax=docker/dockerfile:1
# Copyright 2024, Usman Saleem.
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

# Start from the latest Hyperledger Besu image
FROM hyperledger/besu:latest

# Switch to root to install packages
USER 0

# Install additional packages for SoftHSM2 and OpenSC
RUN apt-get update && \
apt-get install -y --no-install-recommends \
openssl \
libssl3 \
softhsm2 \
opensc \
gnutls-bin && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Create a directory for SoftHSM2 tokens. This can be overridden using a volume mount to persist.
RUN mkdir -p /var/lib/tokens && chmod 755 /var/lib/tokens && chown besu:besu /var/lib/tokens

# Switch back to the besu user
USER besu

# Update workdir to Besu home directory
WORKDIR /opt/besu

# Set environment variables for SoftHSM2 configuration
ENV SOFTHSM2_CONF=/opt/besu/softhsm2.conf

# Copy the PKCS11 plugin JAR to the plugins directory
COPY --chown=besu:besu ./build/libs/besu-pkcs11-plugin-*.jar ./plugins/

# Copy the initialization script
COPY --chown=besu:besu --chmod=755 ./docker/scripts/entrypoint.sh ./entrypoint.sh

# Create a custom SoftHSM2 configuration file in besu home directory
RUN echo "directories.tokendir = /var/lib/tokens" > ./softhsm2.conf

# Set the entrypoint to our new script
ENTRYPOINT ["/opt/besu/entrypoint.sh"]
57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,61 @@ The plugin jar will be available at `build/libs/besu-pkcs11-plugin-<version>.jar

Drop the `besu-pkcs11-plugin-<version>.jar` in the `/plugins` folder under Besu installation. This plugin will expose
following additional cli options:
`TBA`
```shell
--plugin-pkcs11-hsm-config-path=<path>
Path to the PKCS11 configuration file
--plugin-pkcs11-hsm-key-alias=<label>
Alias or label of the private key that is stored in the HSM
--plugin-pkcs11-hsm-password-path=<path>
Path to the file that contains password or PIN to access PKCS11 token
```
The security module provided by this plugin can be loaded with following cli option:
```shell
--security-module=pkcs11-hsm
```

## Linux SoftHSM Setup
Following steps are tested on Ubuntu 24.04 LTS. Install following packages.
`TBA`

## Docker setup
See Dockerfile for details.
- The plugin can be tested as a docker image. The provided [`Dockerfile`](./Dockerfile) is based on Besu's official docker image.
It installs following additional package to manage SECP256K1 private keys and SoftHSM:

```
apt-get install -y --no-install-recommends \
openssl \
libssl3 \
softhsm2 \
opensc \
gnutls-bin
```
- The Dockerfile uses a custom script [`entrypoint.sh`](./docker/scripts/entrypoint.sh) as entrypoint. This script
initializes SoftHSM and generates a private key if required.
- The Dockerfile copies the plugin jar to `/plugins` folder.
- See [Besu documentation](https://besu.hyperledger.org/public-networks/get-started/install/run-docker-image) for
further details about other docker options required to run Besu.
- See the sample [Besu config file](./docker/volumes/config) that defines minimal options required to use the plugin.
- Following is an example to build the docker image:
```shell
docker build --no-cache -t besu-pkcs11:latest .
```
- To run Besu node for testing with SoftHSM, Following directories be mounted as volumes.
Change the path according to your requirements:
- `./docker/volumes/data` for Besu data. It should be mounted to `/var/lib/besu`
- `./docker/volumes/tokens` for SoftHSM data. It should be mounted to `/var/lib/tokens`
- `./docker/volumes/config` for Besu and PKCS11 config files. It MUST be mounted to `/etc/besu/config`. This directory contains the sample configurations.

> [!NOTE]
> To initialize the SoftHSM tokens, the entrypoint script will attempt to generate a SECP256K1 private key and
> initialize SoftHSM on the first run. The SoftHSM `PIN` is defined in `./docker/volumes/config/pkcs11-hsm-password.txt`.
> The `SO_PIN` can be overridden via environment variable, however, it is not required once initialization is done.
- To run the Besu node:
```shell
docker run --rm -it \
-v ./docker/volumes/data:/var/lib/besu \
-v ./docker/volumes/tokens:/var/lib/tokens \
-v ./docker/volumes/config:/etc/besu/config \
besu-pkcs11:latest --config-file=/etc/besu/config/besu-dev.toml
```

## License

Expand Down
3 changes: 3 additions & 0 deletions docker/clean_volumes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#! /bin/sh
rm -rf ./volumes/data
rm -rf ./volumes/tokens
88 changes: 88 additions & 0 deletions docker/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/bin/bash
# Copyright 2024, Usman Saleem.
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

# Set default values for PIN and SO_PIN
DEFAULT_PIN="test123"
DEFAULT_SO_PIN="sotest123"

# Path to the PIN file
PIN_FILE="/etc/besu/config/pkcs11-hsm-password.txt"

# Read PIN from file if it exists, otherwise use environment variable or default value
if [ -f "$PIN_FILE" ]; then
PIN=$(cat "$PIN_FILE")
else
PIN="${PIN:-$DEFAULT_PIN}"
fi

# Use environment variables if set, otherwise use default values
SO_PIN="${SO_PIN:-$DEFAULT_SO_PIN}"

# Set up cleanup trap
trap 'rm -f /tmp/ec-secp256k1-*.pem' EXIT

# Check if SoftHSM module exists
SOFTHSM_MODULE="/usr/lib/softhsm/libsofthsm2.so"
if [ ! -f "$SOFTHSM_MODULE" ]; then
echo "SoftHSM module not found: $SOFTHSM_MODULE"
exit 1
fi

# Check if token already exists
if ! softhsm2-util --show-slots | grep -q "testtoken"; then
echo "Initializing SoftHSM token ..."
if ! softhsm2-util --init-token --slot 0 --label "testtoken" --pin "$PIN" --so-pin "$SO_PIN"; then
echo "Failed to initialize token"
exit 1
fi

echo "Generating SECP256K1 private key using openssl ..."
# Generating temporary SECP256K1 private key (-noout=not encoded)
if ! openssl ecparam -name secp256k1 -genkey -noout -out /tmp/ec-secp256k1-priv-key.pem; then
echo "Failed to generate private key"
exit 1
fi

# Generate public key from private key
if ! openssl ec -in /tmp/ec-secp256k1-priv-key.pem -pubout -out /tmp/ec-secp256k1-pub-key.pem; then
echo "Failed to generate public key"
exit 1
fi

# Generate a self-signed certificate
if ! openssl req -new -x509 -key /tmp/ec-secp256k1-priv-key.pem -out /tmp/ec-secp256k1-cert.pem -days 365 -subj '/CN=example.com'; then
echo "Failed to generate self-signed certificate"
exit 1
fi

echo "Importing openssl secp256k1 key into softhsm id: 1, label: testkey ..."
# Importing private key and cert in softhsm. Note we have to specify --usage-derive for ECDH key agreement to work
if ! pkcs11-tool --module "$SOFTHSM_MODULE" --login --pin "$PIN" \
--write-object /tmp/ec-secp256k1-priv-key.pem --type privkey --usage-derive --id 1 --label "testkey" \
--token-label "testtoken"; then
echo "Failed to import private key"
exit 1
fi

if ! pkcs11-tool --module "$SOFTHSM_MODULE" --login --pin "$PIN" \
--write-object /tmp/ec-secp256k1-pub-key.pem --type pubkey --usage-derive --id 1 --label "testkey" \
--token-label "testtoken"; then
echo "Failed to import public key"
exit 1
fi

if ! pkcs11-tool --module "$SOFTHSM_MODULE" --login --pin "$PIN" \
--write-object /tmp/ec-secp256k1-cert.pem --type cert --id 1 --label "testkey" \
--token-label "testtoken"; then
echo "Failed to import certificate"
exit 1
fi

echo "Token and keys initialized successfully."
else
echo "Token already exists. Skipping initialization."
fi

# Launch Besu with the provided arguments
exec besu "$@"
19 changes: 19 additions & 0 deletions docker/volumes/config/besu-dev.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
network="dev"
miner-enabled=true
miner-coinbase="0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"
rpc-http-cors-origins=["all"]
host-allowlist=["*"]
rpc-ws-enabled=true
rpc-http-enabled=true
data-path="/var/lib/besu"

# plugins options
plugin-pkcs11-hsm-config-path="/etc/besu/config/pkcs11-softhsm.cfg"
plugin-pkcs11-hsm-key-alias="testkey"
plugin-pkcs11-hsm-password-path="/etc/besu/config/pkcs11-hsm-password.txt"

# security module
security-module="pkcs11-hsm"

# Logging
logging="DEBUG"
1 change: 1 addition & 0 deletions docker/volumes/config/pkcs11-hsm-password.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1234
11 changes: 11 additions & 0 deletions docker/volumes/config/pkcs11-softhsm.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name = Softhsm-Besu-SM
library = /usr/lib/softhsm/libsofthsm2.so
# Instead of slot = xxx, use slotListIndex
slotListIndex = 0
showInfo = false

# In order for ECDHA Key Agreement to work, we need following for derived secrets
attributes(generate,CKO_SECRET_KEY,CKK_GENERIC_SECRET) = {
CKA_SENSITIVE = false
CKA_EXTRACTABLE = true
}
17 changes: 12 additions & 5 deletions src/main/java/info/usmans/besu/plugin/softhsm/Pkcs11HsmPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.hyperledger.besu.plugin.BesuPlugin;
import org.hyperledger.besu.plugin.services.PicoCLIOptions;
import org.hyperledger.besu.plugin.services.SecurityModuleService;
import org.hyperledger.besu.plugin.services.securitymodule.SecurityModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -48,11 +49,17 @@ private void registerCliOptions(final BesuContext besuContext) {
*/
private void registerSecurityModule(final BesuContext besuContext) {
// lazy-init our security module implementation during register phase
besuContext
.getService(SecurityModuleService.class)
.orElseThrow(
() -> new IllegalStateException("Expecting SecurityModuleService to be present"))
.register(SECURITY_MODULE_NAME, () -> new Pkcs11SecurityModuleService(cliParams));
final SecurityModuleService securityModuleService =
besuContext
.getService(SecurityModuleService.class)
.orElseThrow(
() -> new IllegalStateException("Expecting SecurityModuleService to be present"));

securityModuleService.register(SECURITY_MODULE_NAME, this::getSecurityModuleSupplier);
}

private SecurityModule getSecurityModuleSupplier() {
return new Pkcs11SecurityModuleService(cliParams);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class Pkcs11PluginCliOptions {
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>")
paramLabel = "<label>")
private String privateKeyAlias;

/** Default constructor. Performs no initialization. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
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;
Expand All @@ -26,12 +25,14 @@
/** A PKCS11 based implementation of Besu SecurityModule interface. */
public class Pkcs11SecurityModuleService implements SecurityModule {
private static final Logger LOG = LoggerFactory.getLogger(Pkcs11SecurityModuleService.class);
private static final String SIGNATURE_ALGORITHM = "NONEWithECDSA";
private static final String KEY_AGREEMENT_ALGORITHM = "ECDH";

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 ...");
Expand Down Expand Up @@ -60,10 +61,22 @@ private void loadPkcs11Provider() {
LOG.info("Initializing PKCS11 provider ...");

try {
provider =
Security.getProvider("SUNPKCS11").configure(cliParams.getPkcs11ConfigPath().toString());
final Provider sunPKCS11Provider = Security.getProvider("SunPKCS11");
if (sunPKCS11Provider == null) {
throw new SecurityModuleException("SunPKCS11 provider not found");
}
// configure the provider with the PKCS11 configuration file
provider = sunPKCS11Provider.configure(cliParams.getPkcs11ConfigPath().toString());
if (provider == null) {
throw new SecurityModuleException("Unable to configure SunPKCS11 provider");
}
// finally add configured provider.
Security.addProvider(provider);
} catch (final Exception e) {
if (e instanceof SecurityModuleException) {
throw (SecurityModuleException) e;
}

throw new SecurityModuleException(
"Error encountered while loading SunPKCS11 provider with configuration: "
+ cliParams.getPkcs11ConfigPath().toString(),
Expand Down Expand Up @@ -136,8 +149,6 @@ private void loadPkcs11PublicKey() {
"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
Expand All @@ -146,7 +157,7 @@ public Signature sign(Bytes32 dataHash) throws SecurityModuleException {
// 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);
java.security.Signature.getInstance(SIGNATURE_ALGORITHM, provider);
signature.initSign(privateKey);
signature.update(dataHash.toArray());
final byte[] sigBytes = signature.sign();
Expand All @@ -169,11 +180,11 @@ public Bytes32 calculateECDHKeyAgreement(PublicKey theirKey) throws SecurityModu
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);
SignatureUtil.eCPointToPublicKey(theirKey.getW(), provider);

// generate ECDH Key Agreement
try {
final KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", provider);
final KeyAgreement keyAgreement = KeyAgreement.getInstance(KEY_AGREEMENT_ALGORITHM, provider);
keyAgreement.init(privateKey);
keyAgreement.doPhase(theirPublicKey, true);
return Bytes32.wrap(keyAgreement.generateSecret());
Expand Down
Loading

0 comments on commit f74b98d

Please sign in to comment.