diff --git a/hsm-samples-gateway/README.md b/hsm-samples-gateway/README.md new file mode 100644 index 00000000..5ec256b3 --- /dev/null +++ b/hsm-samples-gateway/README.md @@ -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 +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` \ No newline at end of file diff --git a/hsm-samples-gateway/go/hsm-sample.go b/hsm-samples-gateway/go/hsm-sample.go new file mode 100644 index 00000000..299fc8d3 --- /dev/null +++ b/hsm-samples-gateway/go/hsm-sample.go @@ -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") +} diff --git a/hsm-samples-gateway/node/.eslintrc.yaml b/hsm-samples-gateway/node/.eslintrc.yaml new file mode 100644 index 00000000..79614105 --- /dev/null +++ b/hsm-samples-gateway/node/.eslintrc.yaml @@ -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 diff --git a/hsm-samples-gateway/node/.gitignore b/hsm-samples-gateway/node/.gitignore new file mode 100644 index 00000000..1a993212 --- /dev/null +++ b/hsm-samples-gateway/node/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +package-lock.json diff --git a/hsm-samples-gateway/node/package.json b/hsm-samples-gateway/node/package.json new file mode 100644 index 00000000..587e7239 --- /dev/null +++ b/hsm-samples-gateway/node/package.json @@ -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" + } +} diff --git a/hsm-samples-gateway/node/src/hsm-sample.ts b/hsm-samples-gateway/node/src/hsm-sample.ts new file mode 100644 index 00000000..bf274a1e --- /dev/null +++ b/hsm-samples-gateway/node/src/hsm-sample.ts @@ -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 { + 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 { + 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); diff --git a/hsm-samples-gateway/node/tsconfig.json b/hsm-samples-gateway/node/tsconfig.json new file mode 100644 index 00000000..ce5ff5ab --- /dev/null +++ b/hsm-samples-gateway/node/tsconfig.json @@ -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/" + ] +} diff --git a/hsm-samples-gateway/scripts/generate-hsm-user.sh b/hsm-samples-gateway/scripts/generate-hsm-user.sh new file mode 100755 index 00000000..3bcff2fd --- /dev/null +++ b/hsm-samples-gateway/scripts/generate-hsm-user.sh @@ -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