Add support for HSM based identities

Add support to the basic application to check for the HSM environment and
enroll and store the certificate and HSM handle of the  HSM identity in the
wallet. The identity will then be retrieved form the wallet providing access
for signing.

Signed-off-by: Bret Harrison <beharrison@nc.rr.com>
This commit is contained in:
Bret Harrison 2020-12-01 12:00:30 -05:00
parent 36b5788bad
commit d999f51dea
6 changed files with 260 additions and 62 deletions

View file

@ -0,0 +1,142 @@
# Asset Transfer Basic javascript Sample
The asset transfer basic sample demonstrates submitting transactions
and queries.
## About the Sample
This javascript sample application is able to use any of the asset transfer basic
chaincode samples. It will show how to enroll an admin identity. The admin will then
register and enroll a user identity to work with the Fabric network.
The application will demostrate the basic operations of create, update, delete,
and query on an asset.
## Running the sample
Use the Fabric test network utility (`network.sh`) to start a Fabric network and deploy one
one of the sample chaincodes.
Follow these step in order.
- Start the test network with a Certificate Authority and create a channel
```
cd test-network
./network.sh up createChannel -c mychannel -ca
```
- Deploy the chaincode (smart contract)
```
# to deploy the javascript version
./network.sh deployCC -ccs 1 -ccv 1 -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -ccl javascript -ccp ./../asset-transfer-basic/chaincode-javascript/ -ccn basic
```
- Run the application
```
cd asset-transfer-basic/application-javascript
npm install
node app.js
```
If you see an error:
```
2020-08-07T20:23:17.590Z - error: [DiscoveryService]: send[mychannel] - Channel:mychannel received discovery error:access denied
******** FAILED to run the application: Error: DiscoveryService: mychannel error: access denied
```
or
```
Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]]
******** FAILED to run the application: Error: Identity not found in wallet: appUser
```
Delete the `/fabric-samples/asset-transfer-basic/application-javascript/wallet` directory
and retry this application.
The certificate authority must have been restarted and the saved certificates for the
admin and application user are not valid. Deleting the wallet store will force these to be reset
with the new certificate authority.
To see the SDK workings, try setting the logging to show on the console before running
```
export HFC_LOGGING='{"debug":"console"}'
```
or log to a file
```
export HFC_LOGGING='{"debug":"./debug.log"}'
```
## Clean up
When you are finished, you can bring down the test network.
The command will remove all the nodes of the test network, and delete any
ledger data that you created:
```
./network.sh down
```
Be sure to delete the wallet directory create by the application.
The stored identities will no longer be valid when restarting the network.
## Configuring and running a Hardware Security Module
The javascript sample application for asset transfer basic includes
support for a HSM. Below are the steps required to run a Hardware Security Module (HSM)
simulator locally.
**_Note:_**
When using the HSM simulator you will only be able to run the application once
without restarting the Fabric network and removing the wallet store.
Rerunning the application will restart the HSM simulator. The user's identity
information that has been saved to the wallet store will not exist in the
restarted HSM simulator.
### Install SoftHSM
In order to run the javascript 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: `apt-get 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.2.0.tar.gz>
3. `tar -xvf softhsm-2.2.0.tar.gz`
4. `cd softhsm-2.2.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`
### Specify the SoftHSM configuration file
The configuration file required for the simulator has been created and is
located in the test network directory. Set the following environment
variable to point to a config file before running the javascript application.
When the javascript application sees the following environment variable, it will
attempt to connect to the HSM simulator.
```bash
export SOFTHSM2_CONF="../test-network/hsm/softhsm2.conf"
```
### Create a token to store keys in the HSM
```bash
softhsm2-util --init-token --slot 0 --label "ForFabric" --pin 98765432 --so-pin 1234
```
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.
### Configuration
By default the javascript sample application will run with SoftHSM using slot `0` and user PIN `98765432`.
If your configuration is different, edit the application or use these environment variables to pass in the values:
* PKCS11_LIB - path to the SoftHSM2 library; if not specified, the application will search a list of common install locations
* PKCS11_PIN
* PKCS11_SLOT

View file

@ -6,77 +6,43 @@
'use strict'; 'use strict';
const { Gateway, Wallets } = require('fabric-network'); /**
* A test application to show basic queries operations with any of the asset-transfer-basic chaincodes
* -- How to submit a transaction
* -- How to query and check the
* -- How to use an HSM for users key and signing support
**/
const { Gateway, Wallets, HsmX509Provider } = require('fabric-network');
const FabricCAServices = require('fabric-ca-client'); const FabricCAServices = require('fabric-ca-client');
const path = require('path'); const path = require('path');
const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); const fs = require('fs');
const { buildCAClient, registerAndEnrollUser, enrollAdmin, getHSMLibPath} = require('../../test-application/javascript/CAUtil.js');
const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js'); const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js');
const channelName = 'mychannel'; const channelName = 'mychannel';
const chaincodeName = 'basic'; const chaincodeName = 'basic';
const mspOrg1 = 'Org1MSP'; const mspOrg1 = 'Org1MSP';
const walletPath = path.join(__dirname, 'wallet'); const walletPath = path.join(__dirname, 'wallet');
const org1UserId = 'appUser';
const user1 = 'appUser1';
const user2 = 'appUser2';
function prettyJSONString(inputString) { function prettyJSONString(inputString) {
return JSON.stringify(JSON.parse(inputString), null, 2); return JSON.stringify(JSON.parse(inputString), null, 2);
} }
// pre-requisites:
// - fabric-sample two organization test-network setup with two peers, ordering service,
// and 2 certificate authorities
// ===> from directory /fabric-samples/test-network
// ./network.sh up createChannel -ca
// - Use any of the asset-transfer-basic chaincodes deployed on the channel "mychannel"
// with the chaincode name of "basic". The following deploy command will package,
// install, approve, and commit the javascript chaincode, all the actions it takes
// to deploy a chaincode to a channel.
// ===> from directory /fabric-samples/test-network
// ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-javascript/ -ccl javascript
// - Be sure that node.js is installed
// ===> from directory /fabric-samples/asset-transfer-basic/application-javascript
// node -v
// - npm installed code dependencies
// ===> from directory /fabric-samples/asset-transfer-basic/application-javascript
// npm install
// - to run this test application
// ===> from directory /fabric-samples/asset-transfer-basic/application-javascript
// node app.js
// NOTE: If you see kind an error like these:
/*
2020-08-07T20:23:17.590Z - error: [DiscoveryService]: send[mychannel] - Channel:mychannel received discovery error:access denied
******** FAILED to run the application: Error: DiscoveryService: mychannel error: access denied
OR
Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]]
******** FAILED to run the application: Error: Identity not found in wallet: appUser
*/
// Delete the /fabric-samples/asset-transfer-basic/application-javascript/wallet directory
// and retry this application.
//
// The certificate authority must have been restarted and the saved certificates for the
// admin and application user are not valid. Deleting the wallet store will force these to be reset
// with the new certificate authority.
//
/**
* A test application to show basic queries operations with any of the asset-transfer-basic chaincodes
* -- How to submit a transaction
* -- How to query and check the results
*
* To see the SDK workings, try setting the logging to show on the console before running
* export HFC_LOGGING='{"debug":"console"}'
*/
async function main() { async function main() {
try { try {
let username = user1;
// build an in memory object with the network configuration (also known as a connection profile) // build an in memory object with the network configuration (also known as a connection profile)
const ccp = buildCCPOrg1(); const ccp = buildCCPOrg1();
// build an instance of the fabric ca services client based on // build an instance of the fabric ca services client based on
// the information in the network configuration // the information in the network configuration
const caClient = buildCAClient(FabricCAServices, ccp, 'ca.org1.example.com'); const caClient = buildCAClient(FabricCAServices, ccp, 'ca.org1.example.com');
let hsmCaClient; // must a different ca client with a HSM crypto suite assigned when enrolling HSM user
// setup the wallet to hold the credentials of the application user // setup the wallet to hold the credentials of the application user
const wallet = await buildWallet(Wallets, walletPath); const wallet = await buildWallet(Wallets, walletPath);
@ -84,9 +50,37 @@ async function main() {
// in a real application this would be done on an administrative flow, and only once // in a real application this would be done on an administrative flow, and only once
await enrollAdmin(caClient, wallet, mspOrg1); await enrollAdmin(caClient, wallet, mspOrg1);
// users will be created using the HSM if SOFTHSM2_CONF and the HSM simulator has been initialized
// Be sure to run the following before starting this application if you wish use the HSM simulator
/*
export SOFTHSM2_CONF="../../test-network/hsm/softhsm2.conf"
softhsm2-util --init-token --slot 0 --label "ForFabric" --pin 98765432 --so-pin 1234
*/
/*
NOTE: You will not be able to re run this application with the same user name when using
the HSM simulator. The simulator has been restarted and will not have the handle
as saved in the "wallet" store.
*/
if (process.env.SOFTHSM2_CONF) {
// setup the wallet provider and the ca client to have the pkcs11 cryptosuite to work with
// the HSM to generate keys and sign transactions
const hsmOptions = {
lib: getHSMLibPath(fs),
pin: process.env.PKCS11_PIN || '98765432',
slot: Number(process.env.PKCS11_SLOT || '0')
};
// build the wallet identity provider and the PKCS11 crypto suite based on the HSM options
const hsmProvider = new HsmX509Provider(hsmOptions);
wallet.getProviderRegistry().addProvider(hsmProvider);
hsmCaClient = buildCAClient(FabricCAServices, ccp, 'ca.org1.example.com', hsmProvider.getCryptoSuite());
username = user2;
}
// in a real application this would be done only when a new user was required to be added // in a real application this would be done only when a new user was required to be added
// and would be part of an administrative flow // and would be part of an administrative flow
await registerAndEnrollUser(caClient, wallet, mspOrg1, org1UserId, 'org1.department1'); // note: when not using an HSM, hsmCaClient will be 'undefined'
await registerAndEnrollUser(caClient, wallet, mspOrg1, username, 'org1.department1', hsmCaClient);
// Create a new gateway instance for interacting with the fabric network. // Create a new gateway instance for interacting with the fabric network.
// In a real application this would be done as the backend server session is setup for // In a real application this would be done as the backend server session is setup for
@ -100,12 +94,15 @@ async function main() {
// signed by this user using the credentials stored in the wallet. // signed by this user using the credentials stored in the wallet.
await gateway.connect(ccp, { await gateway.connect(ccp, {
wallet, wallet,
identity: org1UserId, identity: username,
discovery: { enabled: true, asLocalhost: true } // using asLocalhost as this gateway is using a fabric network deployed locally discovery: { enabled: true, asLocalhost: true } // using asLocalhost as this gateway is using a fabric network deployed locally
}); });
// Build a network instance based on the channel where the smart contract is deployed // Build a network instance based on the channel where the smart contract is deployed
const network = await gateway.getNetwork(channelName); const network = await gateway.getNetwork(channelName);
// show the peers that network (channel) knows about
// This will show all discovered peers and also peers that are in the connection profile
console.log('\n--> Network has been discovered ' + network.getChannel().getEndorsers());
// Get the contract from the network. // Get the contract from the network.
const contract = network.getContract(chaincodeName); const contract = network.getContract(chaincodeName);
@ -172,6 +169,9 @@ async function main() {
console.log('\n--> Evaluate Transaction: ReadAsset, function returns "asset1" attributes'); console.log('\n--> Evaluate Transaction: ReadAsset, function returns "asset1" attributes');
result = await contract.evaluateTransaction('ReadAsset', 'asset1'); result = await contract.evaluateTransaction('ReadAsset', 'asset1');
console.log(`*** Result: ${prettyJSONString(result.toString())}`); console.log(`*** Result: ${prettyJSONString(result.toString())}`);
} catch (runError) {
console.log('**** Error:' + runError);
console.log(runError.stack);
} finally { } finally {
// Disconnect from the gateway when the application is closing // Disconnect from the gateway when the application is closing
// This will close all connections to the network // This will close all connections to the network

View file

@ -169,6 +169,8 @@ jobs:
CHAINCODE_LANGUAGE: typescript CHAINCODE_LANGUAGE: typescript
steps: steps:
- template: templates/install-deps.yml - template: templates/install-deps.yml
- script: sudo apt-get install softhsm2
displayName: Install SoftHSM
- script: ../ci/scripts/run-test-network-basic.sh - script: ../ci/scripts/run-test-network-basic.sh
workingDirectory: test-network workingDirectory: test-network
displayName: Run Test Network Basic Chaincode displayName: Run Test Network Basic Chaincode

View file

@ -48,6 +48,11 @@ pushd ../asset-transfer-basic/application-javascript
npm install npm install
print "Executing app.js" print "Executing app.js"
node app.js node app.js
print "Setup the HSM simulator"
export SOFTHSM2_CONF="../../test-network/hsm/softhsm2.conf"
softhsm2-util --init-token --slot 0 --label "ForFabric" --pin 98765432 --so-pin 1234
print "Executing app.js using the HSM simulator"
node app.js
popd popd
stopNetwork stopNetwork

View file

@ -6,6 +6,10 @@
'use strict'; 'use strict';
// provider types
const HSM_PROVIDER = 'HSM-X.509';
const X509_PROVIDER = 'X.509';
const adminUserId = 'admin'; const adminUserId = 'admin';
const adminUserPasswd = 'adminpw'; const adminUserPasswd = 'adminpw';
@ -14,11 +18,11 @@ const adminUserPasswd = 'adminpw';
* @param {*} FabricCAServices * @param {*} FabricCAServices
* @param {*} ccp * @param {*} ccp
*/ */
exports.buildCAClient = (FabricCAServices, ccp, caHostName) => { exports.buildCAClient = (FabricCAServices, ccp, caHostName, cryptoSuite) => {
// Create a new CA client for interacting with the CA. // Create a new CA client for interacting with the CA.
const caInfo = ccp.certificateAuthorities[caHostName]; //lookup CA details from config const caInfo = ccp.certificateAuthorities[caHostName]; //lookup CA details from config
const caTLSCACerts = caInfo.tlsCACerts.pem; const caTLSCACerts = caInfo.tlsCACerts.pem;
const caClient = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName); const caClient = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName, cryptoSuite);
console.log(`Built a CA Client named ${caInfo.caName}`); console.log(`Built a CA Client named ${caInfo.caName}`);
return caClient; return caClient;
@ -41,7 +45,7 @@ exports.enrollAdmin = async (caClient, wallet, orgMspId) => {
privateKey: enrollment.key.toBytes(), privateKey: enrollment.key.toBytes(),
}, },
mspId: orgMspId, mspId: orgMspId,
type: 'X.509', type: X509_PROVIDER,
}; };
await wallet.put(adminUserId, x509Identity); await wallet.put(adminUserId, x509Identity);
console.log('Successfully enrolled admin user and imported it into the wallet'); console.log('Successfully enrolled admin user and imported it into the wallet');
@ -50,7 +54,7 @@ exports.enrollAdmin = async (caClient, wallet, orgMspId) => {
} }
}; };
exports.registerAndEnrollUser = async (caClient, wallet, orgMspId, userId, affiliation) => { exports.registerAndEnrollUser = async (nonHSMcaClient, wallet, orgMspId, userId, affiliation, hsmCaClient) => {
try { try {
// Check to see if we've already enrolled the user // Check to see if we've already enrolled the user
const userIdentity = await wallet.get(userId); const userIdentity = await wallet.get(userId);
@ -68,11 +72,22 @@ exports.registerAndEnrollUser = async (caClient, wallet, orgMspId, userId, affil
} }
// build a user object for authenticating with the CA // build a user object for authenticating with the CA
const provider = wallet.getProviderRegistry().getProvider(adminIdentity.type); const provider = wallet.getProviderRegistry().getProvider(X509_PROVIDER);
const adminUser = await provider.getUserContext(adminIdentity, adminUserId); const adminUser = await provider.getUserContext(adminIdentity, adminUserId);
// Register the user, enroll the user, and import the new identity into the wallet. // Register the user, enroll the user, and import the new identity into the wallet.
// if affiliation is specified by client, the affiliation value must be configured in CA // if affiliation is specified by client, the affiliation value must be configured in CA
let caClient = nonHSMcaClient;
let type = X509_PROVIDER;
if (hsmCaClient) {
// Will use the HSM CA client which has been initialized with a pkcs11 crypto suite
// to work the HSM for generating keys and signing.
caClient = hsmCaClient;
type = HSM_PROVIDER;
console.log(' ---> Using HSM identity');
}
const secret = await caClient.register({ const secret = await caClient.register({
affiliation: affiliation, affiliation: affiliation,
enrollmentID: userId, enrollmentID: userId,
@ -82,17 +97,44 @@ exports.registerAndEnrollUser = async (caClient, wallet, orgMspId, userId, affil
enrollmentID: userId, enrollmentID: userId,
enrollmentSecret: secret enrollmentSecret: secret
}); });
const x509Identity = { const x509Identity = {
mspId: orgMspId,
type: type,
credentials: { credentials: {
certificate: enrollment.certificate, certificate: enrollment.certificate,
privateKey: enrollment.key.toBytes(), privateKey: enrollment.key.toBytes()
}, }
mspId: orgMspId, }
type: 'X.509',
};
await wallet.put(userId, x509Identity); await wallet.put(userId, x509Identity);
console.log(`Successfully registered and enrolled user ${userId} and imported it into the wallet`); console.log(`Successfully registered and enrolled user ${userId} and imported it into the wallet`);
} catch (error) { } catch (error) {
console.error(`Failed to register user : ${error}`); console.error(`Failed to register user : ${error}`);
} }
}; };
exports.getHSMLibPath = (fs) => {
const pathnames = [
'/usr/lib/softhsm/libsofthsm2.so', // Ubuntu
'/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so', // Ubuntu apt-get install
'/usr/local/lib/softhsm/libsofthsm2.so', // Ubuntu, OSX (tar ball install)
'/usr/lib/libacsp-pkcs11.so' // LinuxOne
];
let pkcsLibPath = 'NOT FOUND';
if (typeof process.env.PKCS11_LIB === 'string' && process.env.PKCS11_LIB !== '') {
pkcsLibPath = process.env.PKCS11_LIB;
} else {
//
// Check common locations for PKCS library
//
for (let i = 0; i < pathnames.length; i++) {
if (fs.existsSync(pathnames[i])) {
pkcsLibPath = pathnames[i];
break;
}
}
}
return pkcsLibPath;
}

View file

@ -0,0 +1,7 @@
# SoftHSM v2 configuration file
directories.tokendir = /tmp/
objectstore.backend = file
# ERROR, WARNING, INFO, DEBUG
log.level = INFO