Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Dockerfile to test the plugin. #10

Merged
merged 3 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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