offline signing of transaction using fabric-common module

582 Views Asked by At

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?

2

There are 2 best solutions below

0
On

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:

  • A Hyperledger Fabric network.
  • A web application. You want this application to implement the Fabric network client, but you don't want it to manage the user private keys itself (for security and/or operational reasons).
  • The web application client. The HTML+JS pages provided from your web app server or maybe a single SPA. You want this client to manage the user private keys, while not implementing the Fabric client (you cannot with plain non-NodeJS Javascript). This client retrieves the unsigned proposal from the web app, signs it and sends the signed proposal to the web app to continue with the process.

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.

0
On

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:

  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"

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"
  }