Hyperledger Fabric Offline Private Key Transaction Signing Flow #fabric #fabric-sdk-node #fabric-offline-tx-signing #hyperledger-fabric


Kavin Arumugam
 

Hi Fabric Community,

I'm exploring the offline transaction signing flow in hyperledger fabric by using Fabric Node SDK.
I came across the following official tutorial. 
https://hyperledger.github.io/fabric-sdk-node/release-2.2/tutorial-sign-transaction-offline.html
In my view, the above tutorial explains the process in a abstract manner.
Also, I'm not able to find the resource/code which will be helpful for me to see the demo of offline private key Tx Signing.
I'm sharing my learnings here. so that, it will be helpful for the fabric community.

Following is the sample code to start with Offline Transaction Signing in Fabric:
Note: Before running the following code, test-network must be up and also the Private Key & CSR must be created (see below for detailed steps)

Fabric Network BringUp, CreateChannel, Deploy Chaincode and Invoke & Query Chaincode in CLI:
Navigate to test-network folder in your fabric-samples repository. Then Execute the following commands in a CLI.

. env.sh

./network.sh up createChannel -ca

./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go

export PATH=${PWD}/../bin:$PATH

export FABRIC_CFG_PATH=$PWD/../config/

export CORE_PEER_TLS_ENABLED=true

export CORE_PEER_LOCALMSPID="Org1MSP"

export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt

export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@.../msp

export CORE_PEER_ADDRESS=localhost:7051

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" --peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" -c '{"function":"InitLedger","Args":[]}'

peer chaincode query -C mychannel -n basic -c '{"Args":["GetAllAssets"]}'



Generating Private Key and Creating CSR:

IMP: CSR must contain the information "common name" and the "common name" must be same as the "enrollmentID" at the register step with CA

openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem

 

openssl req -new -sha256 -key private-key.pem -out csr.pem



Dependencies:

"dependencies": {
"elliptic": "^6.5.4",
"fabric-ca-client": "^2.2.14",
"fabric-common": "^2.2.14",
"fabric-network": "^2.2.14",
"jsrsasign": "^10.5.27"
}




Steps for Offline Private Key Tx Signing Flow:

1) Registering an Identity with CA

2) Creating Private Key and CSR for an Identity

3) Enrolling an Identity with CA but with "csr" parameter. 

4) Storing the identity in the Wallet

5) Prepare Key for Signing

6) Create Client, Channel, User, Identity Context

7) Creating Endorsement Proposal (Build Proposal for a specific Chaincode with IdentityContext and BuildOptions)

8) Hashing the Proposal Bytes with "sha256" Algorithm and with "hex" encoding

9) Creating Signature by signing the "hash" with "key"

10) Form Endorserer Objects

11) Finally, Sign the endorsement with "Signature" we created in step-9, and send to Target Endorsers

12) Form Committer Objects

13) Create new commit using endorsement, build, sign and send to Targets for "committing"



SAMPLE CODE:

/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const FabricCAServices = require('fabric-ca-client');
const { Wallets } = require('fabric-network');
const {Client, User, Endorser, DiscoveryService, Discoverer, Committer} = require('fabric-common');
const fs = require('fs');
const path = require('path');

const elliptic = require('elliptic');
const { KEYUTIL } = require('jsrsasign');
const crypto = require('crypto');

async function main() {
try {


const enrollmentID = 'testUser'
var userEnrollmentSecret = 'testUserpw';
var userEnrollment;


// load the network configuration
const ccpPath = path.resolve(__dirname, '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json');
const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

// Create a new CA client for interacting with the CA.
const caInfo = ccp.certificateAuthorities['ca.org1.example.com'];
const caTLSCACerts = caInfo.tlsCACerts.pem;
const ca = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName);

// Create a new file system based wallet for managing identities.
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = await Wallets.newFileSystemWallet(walletPath);
console.log(`Wallet path: ${walletPath}`);

// Check to see if we've already enrolled an user.
const userIdentity = await wallet.get(enrollmentID);
if(userIdentity){
console.log('An identity for an user already exists in the wallet');
// Need to do: userEnrollment = userIdentity.getEnrollmentCertificate;

} else {

// Check to see if we've already enrolled the admin user.
const adminIdentity = await wallet.get('admin');
if (!adminIdentity) {
console.log('An identity for the admin user "admin" does not exists in the wallet. Enrolling Now...');
// Enroll the admin user, and import the new identity into the wallet.
const enrollment = await ca.enroll({ enrollmentID: 'admin', enrollmentSecret: 'adminpw' });
const x509Identity = {
credentials: {
certificate: enrollment.certificate,
privateKey: enrollment.key.toBytes(),
},
mspId: 'Org1MSP',
type: 'X.509',
};
await wallet.put('admin', x509Identity);
console.log('Successfully enrolled admin user "admin" and imported it into the wallet');
}

const adminId = await wallet.get('admin');
// build a user object for authenticating with the CA
const provider = wallet.getProviderRegistry().getProvider(adminId.type);
const adminUser = await provider.getUserContext(adminId, 'admin');

// Register the user
const userEnrollSecret = await ca.register({
affiliation: 'org1.department1',
enrollmentID: enrollmentID,
enrollmentSecret: userEnrollmentSecret,
role: 'client'
}, adminUser);

// Read CSR from File system
const csr = fs.readFileSync('csr.pem', 'utf8');
const req = {
enrollmentID: enrollmentID,
enrollmentSecret: userEnrollmentSecret,
csr: csr,
};
// Enroll the user with CSR, and import the new identity into the wallet.
userEnrollment = await ca.enroll(req);
const x509Identity = {
credentials: {
certificate: userEnrollment.certificate
},
mspId: 'Org1MSP',
type: 'X.509',
};


await wallet.put(enrollmentID, x509Identity);
console.log('Successfully enrolled an User and imported it into the wallet');
console.log(userEnrollment.certificate)
console.log(userEnrollment.key)

}

 
// This is a sample code for signing the digest from step 2 with EC.
// Different signature algorithm may have different interfaces
// ECDSA -- ASN1 OID: prime256v1 -- NIST CURVE: P-256 -- Signature Algorithm: ecdsa-with-SHA256 --

const privateKeyPEM = fs.readFileSync("private-key.pem", "utf8");
console.log("My key is: ", privateKeyPEM);
const { prvKeyHex } = KEYUTIL.getKey(privateKeyPEM); // convert the pem encoded key to hex encoded private key

const EC = elliptic.ec;
const ecdsaCurve = elliptic.curves['p256'];

const ecdsa = new EC(ecdsaCurve);
const signKey = ecdsa.keyFromPrivate(prvKeyHex, 'hex');


// Creating Client, Identity Context, etc
const client = new Client('myclient');
const channel = client.newChannel('mychannel');
const user = User.createUser(enrollmentID, userEnrollmentSecret, 'Org1MSP', userEnrollment.certificate, privateKeyPEM);
const idx = client.newIdentityContext(user);

// To get Service Discovery Results if suppose we need to get dynamic Peer and Orderer Info
// Right now - static peer and orderer objects were used
const discoverer = new Discoverer("peer0", client, "Org1MSP");
const endpoint = client.newEndpoint({
url: 'grpcs://localhost:7051',
pem : '-----BEGIN CERTIFICATE-----\n' +
'MIICJzCCAc2gAwIBAgIUa10ti6LkZFxoLszlnVvzkNdS3OAwCgYIKoZIzj0EAwIw\n' +
'cDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQH\n' +
'EwZEdXJoYW0xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\n' +
'Lm9yZzEuZXhhbXBsZS5jb20wHhcNMjIwOTA0MDExOTAwWhcNMzcwODMxMDExOTAw\n' +
'WjBwMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExDzANBgNV\n' +
'BAcTBkR1cmhhbTEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMT\n' +
'Y2Eub3JnMS5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFrx\n' +
'H9SZ/D8HKPDbrg3YY2Q+qyj5Dw/kHKcH4PErUNUNssLEi1SkovkgWda1sxcNpBCi\n' +
'NgnykaU3tMuMcvBm3MyjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n' +
'AQH/AgEBMB0GA1UdDgQWBBTtq/lyW3VgSOAsty21Q5/4f/k4OTAKBggqhkjOPQQD\n' +
'AgNIADBFAiEA7PFPsmyplE991kF25h+UmscOA1xqDPsYxIAL4QEjXK4CIHxDyUjG\n' +
'RCtIvuZbtg80j2Rtchn+shF5afcIJqCzbcqI\n' +
'-----END CERTIFICATE-----\n',
"ssl-target-name-override" : 'peer0.org1.example.com',
requestTimeout: 3000
});
discoverer.setEndpoint(endpoint);
// await discoverer.connect()


const discovery = new DiscoveryService("basic", channel);
// const endorsement1 = channel.newEndorsement("basic");
// discovery.build(idx, {endorsement: endorsement1});
discovery.build(idx);
discovery.sign(idx);

const discovery_results = await discovery.send({targets: [discoverer], asLocalhost: true});
console.log(JSON.stringify(discovery_results))

 

// Creating Proposal
const endorsement = channel.newEndorsement("basic");
const build_options = {fcn: 'TransferAsset', args: ['asset2', 'Kavin']};
const proposalBytes = endorsement.build(idx, build_options);


// Calculate Hash for transaction Proposal Bytes
const hash = crypto.createHash('sha256').update(proposalBytes).digest('hex');


// Creating Signature
const sig = ecdsa.sign(Buffer.from(hash, 'hex'), signKey, { canonical: true });
const signature = Buffer.from(sig.toDER());
console.log('signature:', signature)

// Endorserer Objects
const peer0Org1Endorser = new Endorser("peer0Org1", client, "Org1MSP");
const peer0Org1Endpoint = client.newEndpoint({
url: 'grpcs://localhost:7051',
pem : '-----BEGIN CERTIFICATE-----\n' +
'MIICJzCCAc2gAwIBAgIUa10ti6LkZFxoLszlnVvzkNdS3OAwCgYIKoZIzj0EAwIw\n' +
'cDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQH\n' +
'EwZEdXJoYW0xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\n' +
'Lm9yZzEuZXhhbXBsZS5jb20wHhcNMjIwOTA0MDExOTAwWhcNMzcwODMxMDExOTAw\n' +
'WjBwMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExDzANBgNV\n' +
'BAcTBkR1cmhhbTEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMT\n' +
'Y2Eub3JnMS5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFrx\n' +
'H9SZ/D8HKPDbrg3YY2Q+qyj5Dw/kHKcH4PErUNUNssLEi1SkovkgWda1sxcNpBCi\n' +
'NgnykaU3tMuMcvBm3MyjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n' +
'AQH/AgEBMB0GA1UdDgQWBBTtq/lyW3VgSOAsty21Q5/4f/k4OTAKBggqhkjOPQQD\n' +
'AgNIADBFAiEA7PFPsmyplE991kF25h+UmscOA1xqDPsYxIAL4QEjXK4CIHxDyUjG\n' +
'RCtIvuZbtg80j2Rtchn+shF5afcIJqCzbcqI\n' +
'-----END CERTIFICATE-----\n',
"ssl-target-name-override" : 'peer0.org1.example.com',
requestTimeout: 3000
});

peer0Org1Endorser.setEndpoint(peer0Org1Endpoint);
await peer0Org1Endorser.connect();
console.log("peer0Org1Endorser status: ", await peer0Org1Endorser.checkConnection())


const peer0Org2Endorser = new Endorser("peer0Org2", client, "Org2MSP");
const peer0Org2Endpoint = client.newEndpoint({
url: 'grpcs://localhost:9051',
pem : '-----BEGIN CERTIFICATE-----\n' +
'MIICHzCCAcWgAwIBAgIUHcDOiu0zeZoOuyE20TgmAIAeahEwCgYIKoZIzj0EAwIw\n' +
'bDELMAkGA1UEBhMCVUsxEjAQBgNVBAgTCUhhbXBzaGlyZTEQMA4GA1UEBxMHSHVy\n' +
'c2xleTEZMBcGA1UEChMQb3JnMi5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eub3Jn\n' +
'Mi5leGFtcGxlLmNvbTAeFw0yMjA5MDQwMTE5MDBaFw0zNzA4MzEwMTE5MDBaMGwx\n' +
'CzAJBgNVBAYTAlVLMRIwEAYDVQQIEwlIYW1wc2hpcmUxEDAOBgNVBAcTB0h1cnNs\n' +
'ZXkxGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2NhLm9yZzIu\n' +
'ZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASuc9tSZ1VhaGCL\n' +
'Z6msge/UIo4jcn1vwpvgQ7Ih8h9FpypQeYqY5DNWLIzgMRD13wSQK8smvfcWQuW1\n' +
'SqNzQu9Po0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd\n' +
'BgNVHQ4EFgQUzIcp1SbKBNLz3owIypQlg4Z5QdwwCgYIKoZIzj0EAwIDSAAwRQIh\n' +
'AILfLS4GgZYWVtR+MF25xrYRtkAkDhsNKZgsBzKlmHn0AiAmGDRBQ+JhcrOuiORn\n' +
'ghA0uKRpUa/JQbihG85bbXm1Kw==\n' +
'-----END CERTIFICATE-----\n',
"ssl-target-name-override" : 'peer0.org2.example.com',
requestTimeout: 3000
});

peer0Org2Endorser.setEndpoint(peer0Org2Endpoint);
await peer0Org2Endorser.connect();
console.log("peer0Org2Endorser status: ", await peer0Org2Endorser.checkConnection())


// Final - Sending Proposal Request
endorsement.sign(signature);
const proposalResponses = await endorsement.send({targets : [peer0Org1Endorser, peer0Org2Endorser]});
console.log(proposalResponses.responses);
 
 
// Committer Objects
const newCommitter = new Committer("orderer.example.com", client, "OrdererMSP");
const newCommitterEndpoint = client.newEndpoint({
url: 'grpcs://localhost:7050',
pem : '-----BEGIN CERTIFICATE-----\n' +
'MIICCzCCAbGgAwIBAgIUDI2rLaEJAyTPibHGw4xk3gXALnYwCgYIKoZIzj0EAwIw\n' +
'YjELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcg\n' +
'WW9yazEUMBIGA1UEChMLZXhhbXBsZS5jb20xFzAVBgNVBAMTDmNhLmV4YW1wbGUu\n' +
'Y29tMB4XDTIyMDkwNDAxMTkwMFoXDTM3MDgzMTAxMTkwMFowYjELMAkGA1UEBhMC\n' +
'VVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazEUMBIGA1UE\n' +
'ChMLZXhhbXBsZS5jb20xFzAVBgNVBAMTDmNhLmV4YW1wbGUuY29tMFkwEwYHKoZI\n' +
'zj0CAQYIKoZIzj0DAQcDQgAEUneOJ/VC/2dZkkVJqtrHo+8hBkLnRnxoCQI0y+Sh\n' +
'yrFErNiL7XHCbHRglIoULixoGdcLCo2COOhQrHfMjyc7TqNFMEMwDgYDVR0PAQH/\n' +
'BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFK7gKwF7q/ByOeyr\n' +
'd/qev66CN8OfMAoGCCqGSM49BAMCA0gAMEUCIQDulgwk7Nt/U92BB2QSEdDx6hG+\n' +
'SBypZMmV7o5RWUugMAIgUaQuk9g9g+s1BtbFvlRTfmBP2oaZZiKPp2+iKVfzE+4=\n' +
'-----END CERTIFICATE-----\n',
"ssl-target-name-override" : 'orderer.example.com',
requestTimeout: 3000
});

newCommitter.setEndpoint(newCommitterEndpoint);
await newCommitter.connect();
console.log("Committer Connection Status: ", await newCommitter.checkConnection())


// Commit the Transaction
const commitReq = endorsement.newCommit();
commitReq.build(idx);
commitReq.sign(idx);
const res = await commitReq.send({targets : [newCommitter]});

console.log("Commit Result: ", res)



} catch (error) {
console.error(`Failed to enroll admin user "admin": ${error}`);
process.exit(1);
}
}

main();