Added hsmm Samples using Gateway

Signed-off-by: sapthasurendran <saptha.surendran@ibm.com>
This commit is contained in:
sapthasurendran 2022-06-07 22:10:24 +05:30
parent 8ca50df4ff
commit ce8d1ddc5b
8 changed files with 556 additions and 0 deletions

View file

@ -0,0 +1,100 @@
# Fabric Gateway HSM Samples
The samples show how to create client applications that invoke transactions with HSM Identities using the
new embedded Gateway in Fabric.
The samples will only run against Fabric v2.4 and higher.
This will create a local docker network comprising five peers across three organisations and a single ordering node.
Sample client applications are available to demonstrate the features of the Fabric Gateway and associated SDKs using this network.
## C Compilers
In order for the client application to run successfully you must ensure you have C compilers and Python 3 (Note that Python 2 may still work however Python 2 is out of support and could stop working in the future) installed otherwise the node dependency `pkcs11js` will not be built and the application will fail. The failure will have an error such as
```
Error: Cannot find module 'pkcs11js'
```
how to install the required C Compilers and Python will depend on your operating system and version.
## Install SoftHSM
In order to run the application in the absence of a real HSM, a software
emulator of the PKCS#11 interface is required.
For more information please refer to [SoftHSM](https://www.opendnssec.org/softhsm/).
SoftHSM can either be installed using the package manager for your host system:
* Ubuntu: `sudo apt install softhsm2`
* macOS: `brew install softhsm`
* Windows: **unsupported**
Or compiled and installed from source:
1. install openssl 1.0.0+ or botan 1.10.0+
2. download the source code from <https://dist.opendnssec.org/source/softhsm-2.5.0.tar.gz>
3. `tar -xvf softhsm-2.5.0.tar.gz`
4. `cd softhsm-2.5.0`
5. `./configure --disable-gost` (would require additional libraries, turn it off unless you need 'gost' algorithm support for the Russian market)
6. `make`
7. `sudo make install`
## Initialize a token to store keys in SoftHSM
If you have not initialized a token previously (or it has been deleted) then you will need to perform this one time operation
```bash
echo directories.tokendir = /tmp > $HOME/softhsm2.conf
export SOFTHSM2_CONF=$HOME/softhsm2.conf
softhsm2-util --init-token --slot 0 --label "ForFabric" --pin 98765432 --so-pin 1234
```
This will create a SoftHSM configuration file called `softhsm2.conf` and will be stored in your home directory. This is
where the sample expects to find a SoftHSM configuration file
The Security Officer PIN, specified with the `--so-pin` flag, can be used to re-initialize the token,
and the user PIN (see below), specified with the `--pin` flag, is used by applications to access the token for
generating and retrieving keys.
## Install PKCS#11 enabled fabric-ca-client binary
To be able to register and enroll identities using an HSM you need a PKCS#11 enabled version of `fabric-ca-client`
To install this use the following command
```bash
go get -tags 'pkcs11' github.com/hyperledger/fabric-ca/cmd/fabric-ca-client
```
## Enroll the HSM User
A user, `HSMUser`, who is HSM managed needs to be registered then enrolled for the sample
```bash
cd scripts
./generate-hsm-user.sh HSMUser
```
This will register a user `HSMUser` with the CA in Org1 (if not already registered) and then enroll that user which will
generate a certificate on the file system for use by the sample. The private key is stored in SoftHSM
### Go SDK
For HSM support you need to ensure you include the `pkcs11` build tag.
```
cd hsm-samples-gateway/go
go run -tags pkcs11 hsm-sample.go
```
### Node SDK
```
cd hsm-samples-gateway/node
npm install
npm run build
npm start
```
When you are finished running the samples, the local docker network can be brought down with the following command:
`docker rm -f $(docker ps -aq) && docker network prune --force`

View file

@ -0,0 +1,187 @@
//go:build pkcs11
// +build pkcs11
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/pem"
"errors"
"os"
"crypto/x509"
"fmt"
"io/ioutil"
"time"
"github.com/hyperledger/fabric-gateway/pkg/client"
"github.com/hyperledger/fabric-gateway/pkg/identity"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
mspID = "Org1MSP"
cryptoPath = "../../scenario/fixtures/crypto-material/"
certPath = cryptoPath + "hsm/HSMUser/signcerts/cert.pem"
tlsCertPath = cryptoPath + "crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"
peerEndpoint = "localhost:7051"
)
func main() {
fmt.Println("Running the GO HSM Sample")
// The gRPC client connection should be shared by all Gateway connections to this endpoint
clientConnection := newGrpcConnection()
defer clientConnection.Close()
hsmSignerFactory, err := identity.NewHSMSignerFactory(findSoftHSMLibrary())
if err != nil {
panic(err)
}
defer hsmSignerFactory.Dispose()
certificatePEM, err := ioutil.ReadFile(certPath)
if err != nil {
panic(err)
}
id := newIdentity(certificatePEM)
ski := getSKI(certificatePEM)
hsmSign, hsmSignClose := newHSMSign(hsmSignerFactory, ski)
defer hsmSignClose()
// Create a Gateway connection for a specific client identity
gateway, err := client.Connect(id, client.WithSign(hsmSign), client.WithClientConnection(clientConnection))
if err != nil {
panic(err)
}
defer gateway.Close()
exampleSubmit(gateway)
fmt.Println()
fmt.Println("Go HSM Sample Completed Successfully")
fmt.Println()
}
func exampleSubmit(gateway *client.Gateway) {
network := gateway.GetNetwork("mychannel")
contract := network.GetContract("basic")
timestamp := time.Now().String()
fmt.Printf("Submitting \"put\" transaction with arguments: time, %s\n", timestamp)
// Submit transaction, blocking until the transaction has been committed on the ledger
submitResult, err := contract.SubmitTransaction("put", "time", timestamp)
if err != nil {
panic(fmt.Errorf("failed to submit transaction: %w", err))
}
fmt.Printf("Submit result: %s\n", string(submitResult))
fmt.Println("Evaluating \"get\" query with arguments: time")
evaluateResult, err := contract.EvaluateTransaction("get", "time")
if err != nil {
panic(fmt.Errorf("failed to evaluate transaction: %w", err))
}
fmt.Printf("Query result = %s\n", string(evaluateResult))
}
// newGrpcConnection creates a gRPC connection to the Gateway server.
func newGrpcConnection() *grpc.ClientConn {
certificate, err := loadCertificate(tlsCertPath)
if err != nil {
panic(fmt.Errorf("failed to obtain commit status: %w", err))
}
certPool := x509.NewCertPool()
certPool.AddCert(certificate)
transportCredentials := credentials.NewClientTLSFromCert(certPool, "peer0.org1.example.com")
connection, err := grpc.Dial(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
if err != nil {
panic(fmt.Errorf("failed to evaluate transaction: %w", err))
}
return connection
}
// newIdentity creates a client identity for this Gateway connection using an X.509 certificate.
func newIdentity(certificatePEM []byte) *identity.X509Identity {
cert, err := identity.CertificateFromPEM(certificatePEM)
if err != nil {
panic(err)
}
id, err := identity.NewX509Identity(mspID, cert)
if err != nil {
panic(err)
}
return id
}
// newHSMSign creates a function that generates a digital signature from a message digest using a private key.
func newHSMSign(h *identity.HSMSignerFactory, certPEM []byte) (identity.Sign, identity.HSMSignClose) {
opt := identity.HSMSignerOptions{
Label: "ForFabric",
Pin: "98765432",
Identifier: string(certPEM),
}
sign, close, err := h.NewHSMSigner(opt)
if err != nil {
panic(err)
}
return sign, close
}
func loadCertificate(filename string) (*x509.Certificate, error) {
certificatePEM, err := ioutil.ReadFile(filename) //#nosec G304
if err != nil {
return nil, err
}
return identity.CertificateFromPEM(certificatePEM)
}
func getSKI(certPEM []byte) []byte {
block, _ := pem.Decode(certPEM)
x590cert, _ := x509.ParseCertificate(block.Bytes)
pk := x590cert.PublicKey
return skiForKey(pk.(*ecdsa.PublicKey))
}
func skiForKey(pk *ecdsa.PublicKey) []byte {
ski := sha256.Sum256(elliptic.Marshal(pk.Curve, pk.X, pk.Y))
return ski[:]
}
func findSoftHSMLibrary() string {
libraryLocations := []string{
"/usr/lib/softhsm/libsofthsm2.so",
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
"/usr/local/lib/softhsm/libsofthsm2.so",
"/usr/lib/libacsp-pkcs11.so",
}
for _, libraryLocation := range libraryLocations {
if _, err := os.Stat(libraryLocation); !errors.Is(err, os.ErrNotExist) {
return libraryLocation
}
}
panic("No SoftHSM library can be found. The Sample requires SoftHSM to be installed")
}

View file

@ -0,0 +1,29 @@
env:
node: true
es2020: true
root: true
ignorePatterns:
- dist/
extends:
- eslint:recommended
rules:
indent:
- error
- 4
quotes:
- error
- single
overrides:
- files:
- "**/*.ts"
parser: "@typescript-eslint/parser"
parserOptions:
sourceType: module
ecmaFeatures:
impliedStrict: true
plugins:
- "@typescript-eslint"
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended

3
hsm-samples-gateway/node/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
dist/
node_modules/
package-lock.json

View file

@ -0,0 +1,35 @@
{
"name": "gateway-hsm-sample",
"version": "0.0.1",
"description": "",
"main": "dist/hsm-sample.js",
"engines": {
"node": "^14.15.0 || ^16.13.0"
},
"scripts": {
"build": "npm-run-all clean compile lint",
"clean": "rimraf dist",
"compile": "tsc",
"lint": "eslint . --ext .ts",
"start": "SOFTHSM2_CONF=${HOME}/softhsm2.conf node dist/hsm-sample.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.3.0",
"@hyperledger/fabric-gateway": "file:../../node/fabric-gateway-dev.tgz",
"jsrsasign": "^10.3.0"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.1",
"@types/jsrsasign": "^9.0.3",
"@types/node": "^14.17.32",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"eslint": "^8.1.0",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"typescript": "~4.5.4"
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import * as grpc from '@grpc/grpc-js';
import * as crypto from 'crypto';
import { connect, Gateway, HSMSigner, HSMSignerFactory, HSMSignerOptions, signers } from '@hyperledger/fabric-gateway';
import * as fs from 'fs';
import * as jsrsa from 'jsrsasign';
import * as path from 'path';
const mspId = 'Org1MSP';
const user = 'HSMUser';
// Sample uses fabric-ca-client generated HSM identities, certificate is located in the signcerts directory
// and has been stored in a directory of the name given to the identity.
const cryptoPath = path.resolve(__dirname, '..', '..', '..', 'scenario', 'fixtures', 'crypto-material');
const certPath = path.resolve(cryptoPath, 'hsm', user, 'signcerts', 'cert.pem');
const tlsCertPath = path.resolve(cryptoPath, 'crypto-config', 'peerOrganizations', 'org1.example.com', 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt');
const peerEndpoint = 'localhost:7051'
async function main() {
console.log('\nRunning the Node HSM sample');
// The gRPC client connection should be shared by all Gateway connections to this endpoint
const client = await newGrpcConnection();
// get an HSMSigner Factory. You only need to do this once for the application
const hsmSignerFactory = signers.newHSMSignerFactory(findSoftHSMPKCS11Lib());
const credentials = await fs.promises.readFile(certPath);
// Get the signer function and a close function. The close function closes the signer
// once there is no further need for it.
const {signer, close} = await newHSMSigner(hsmSignerFactory, credentials.toString());
const gateway = connect({
client,
identity: {mspId, credentials},
signer,
});
try {
await exampleSubmit(gateway);
console.log();
console.log('Node HSM sample completed successfully');
} finally {
gateway.close();
client.close();
// close the HSM Signer
close();
}
}
async function exampleSubmit(gateway: Gateway) {
const network = gateway.getNetwork('mychannel');
const contract = network.getContract('basic');
const timestamp = new Date().toISOString();
console.log('Submitting "put" transaction with arguments: time,', timestamp);
// Submit a transaction, blocking until the transaction has been committed on the ledger
const submitResult = await contract.submitTransaction('put', 'time', timestamp);
console.log('Submit result:', submitResult.toString());
console.log('Evaluating "get" query with arguments: time');
const evaluateResult = await contract.evaluateTransaction('get', 'time');
console.log('Query result:', evaluateResult.toString());
}
async function newGrpcConnection(): Promise<grpc.Client> {
const tlsRootCert = await fs.promises.readFile(tlsCertPath);
const tlsCredentials = grpc.credentials.createSsl(tlsRootCert);
return new grpc.Client(peerEndpoint, tlsCredentials, {
'grpc.ssl_target_name_override': 'peer0.org1.example.com'
});
}
// Create a new HSM Signer
async function newHSMSigner(hsmSignerFactory: HSMSignerFactory, certificatePEM: string): Promise<HSMSigner> {
const ski = getSKIFromCertificate(certificatePEM);
// Options for the signer based on using SoftHSM with Token initialized as follows
// softhsm2-util --init-token --slot 0 --label "ForFabric" --pin 98765432 --so-pin 1234
const hsmSignerOptions: HSMSignerOptions = {
label: 'ForFabric',
pin: '98765432',
identifier: ski
}
return hsmSignerFactory.newSigner(hsmSignerOptions);
}
// Utility to find the SoftHSM PKCS11 library as it's location can vary based on
// operating system and version
function findSoftHSMPKCS11Lib(): string {
const commonSoftHSMPathNames = [
'/usr/lib/softhsm/libsofthsm2.so',
'/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so',
'/usr/local/lib/softhsm/libsofthsm2.so',
'/usr/lib/libacsp-pkcs11.so',
];
for (const pathnameToTry of commonSoftHSMPathNames) {
if (fs.existsSync(pathnameToTry)) {
return pathnameToTry
}
}
throw new Error('Unable to find PKCS11 library')
}
// fabric-ca-client set's the CKA_ID of the public/private keys in the HSM to a generated SKI
// value. This function replicates that calculation from a certificate PEM so that the HSM
// object associated with the certificate can be found
function getSKIFromCertificate(certificatePEM: string): Buffer {
const key = jsrsa.KEYUTIL.getKey(certificatePEM);
const uncompressedPoint = getUncompressedPointOnCurve(key as jsrsa.KJUR.crypto.ECDSA);
const hashBuffer = crypto.createHash('sha256');
hashBuffer.update(uncompressedPoint);
const digest = hashBuffer.digest('hex');
return Buffer.from(digest, 'hex');
}
function getUncompressedPointOnCurve(key: jsrsa.KJUR.crypto.ECDSA): Buffer {
const xyhex = key.getPublicKeyXYHex();
const xBuffer = Buffer.from(xyhex.x, 'hex');
const yBuffer = Buffer.from(xyhex.y, 'hex');
const uncompressedPrefix = Buffer.from('04', 'hex');
const uncompressedPoint = Buffer.concat([uncompressedPrefix, xBuffer, yBuffer]);
return uncompressedPoint;
}
main().catch(console.error);

View file

@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/"
]
}

View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -eo pipefail
# define the CA setup
CA_HOST=127.0.0.1
CA_URL=${CA_HOST}:7054
# try to locate the Soft HSM library
POSSIBLE_LIB_LOC=('/usr/lib/softhsm/libsofthsm2.so' \
'/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so' \
'/usr/local/lib/softhsm/libsofthsm2.so' \
'/usr/lib/libacsp-pkcs11.so'
)
for TEST_LIB in "${POSSIBLE_LIB_LOC[@]}"
do
if [ -f $TEST_LIB ]; then
HSM2_LIB=$TEST_LIB
break
fi
done
[ -z $HSM2_LIB ] && echo No SoftHSM PKCS11 Library found, ensure you have installed softhsm2 && exit 1
# create a softhsm2.conf file if one doesn't exist
HSM2_CONF=$HOME/softhsm2.conf
[ ! -f $HSM2_CONF ] && echo directories.tokendir = /tmp > $HSM2_CONF
# Update the client config file to point to the softhsm pkcs11 library
# which must be in $HOME/softhsm directory
CLIENT_CONFIG_TEMPLATE=./ca-client-config/fabric-ca-client-config-template.yaml
CLIENT_CONFIG=./ca-client-config/fabric-ca-client-config.yaml
cp $CLIENT_CONFIG_TEMPLATE $CLIENT_CONFIG
sed -i s+REPLACE_ME_HSMLIB+${HSM2_LIB}+g $CLIENT_CONFIG
# create the users, remove any existing users
CRYPTO_PATH=$PWD/crypto-material/hsm
[ -d $CRYPTO_PATH ] && rm -fr $CRYPTO_PATH
# user passed in as parameter
CAADMIN=admin
CAADMIN_PW=adminpw
HSMUSER=$1
SOFTHSM2_CONF=$HSM2_CONF fabric-ca-client enroll -c $CLIENT_CONFIG -u http://$CAADMIN:$CAADMIN_PW@$CA_URL --mspdir $CRYPTO_PATH/$CAADMIN --csr.hosts example.com
! SOFTHSM2_CONF=$HSM2_CONF fabric-ca-client register -c $CLIENT_CONFIG --mspdir $CRYPTO_PATH/$CAADMIN --id.name $HSMUSER --id.secret $HSMUSER --id.type client --caname ca-org1 --id.maxenrollments 0 -m example.com -u http://$CA_URL && echo user probably already registered, continuing
SOFTHSM2_CONF=$HSM2_CONF fabric-ca-client enroll -c $CLIENT_CONFIG -u http://$HSMUSER:$HSMUSER@$CA_URL --mspdir $CRYPTO_PATH/$HSMUSER --csr.hosts example.com