Are there any solutions available that the user transfer the signed content of the transaction(transaction proposal) to Blockchain, signed with private key of the registered user on the web, so that the users continue to work through the web application only?
offline signing of transaction using fabric-common module
587 Views Asked by sohaib ali gill AtThere are 2 best solutions below

Following is the sample code to start with Offline Transaction Signing in Fabric v2.2.x.
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/[email protected]/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
Note: Before executing following commands, navigate to a directory where you are going to execute the following sample code.
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
openssl req -new -sha256 -key private-key.pem -out csr.pem
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 = 'testUser1'
var userEnrollmentSecret = 'testUser1pw';
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();
Steps for Offline Private Key Tx Signing Flow:
Registering an Identity with CA
Creating Private Key and CSR for an Identity
Enrolling an Identity with CA but with "csr" parameter.
Storing the identity in the Wallet
Prepare Key for Signing
Create Client, Channel, User, Identity Context
Creating Endorsement Proposal (Build Proposal for a specific Chaincode with IdentityContext and BuildOptions)
Hashing the Proposal Bytes with "sha256" Algorithm and with "hex" encoding
Creating Signature by signing the "hash" with "key"
Form Endorserer Objects
Finally, Sign the endorsement with "Signature" we created in step-9, and send to Target Endorsers
Form Committer Objects
Create new commit using endorsement, build, sign and send to Targets for "committing"
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"
}
Not totally sure about what are you trying to do (you don't give much information), but what I have understood that you have something like:
The offline signature is described here. Your application workflow should delegate step 3 to your client, that it is only responsible of signing the proposal (using the suitable plain Javascript library and managing your keys locally). It is easy to say, but complex to implement in your application workflow cleanly.
Maybe you can check how it is implemented in this project.