Module 5: Advanced Topics and Use Cases
5.2 Advanced Security Features
Hyperledger Fabric provides several advanced security features that go beyond basic blockchain security. This section explores these features and how to implement them in your network.
Private Data Collections
Private Data Collections allow a defined subset of organizations on a channel to endorse, commit, and query private data without having to create a separate channel.
Private Data Collection Configuration
# collections_config.json
[
{
"name": "collectionMarbles",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive": 100000,
"memberOnlyRead": true,
"memberOnlyWrite": true,
"endorsementPolicy": {
"signaturePolicy": "OR('Org1MSP.member', 'Org2MSP.member')"
}
},
{
"name": "collectionMarblePrivateDetails",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive": 3,
"memberOnlyRead": true,
"memberOnlyWrite": true,
"endorsementPolicy": {
"signaturePolicy": "OR('Org1MSP.member')"
}
}
]
Implementing Private Data in Chaincode
// Example of chaincode using private data collections
package main
import (
"encoding/json"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// SmartContract provides functions for managing private data
type SmartContract struct {
contractapi.Contract
}
// Marble describes a marble with public properties
type Marble struct {
ID string `json:"id"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
// MarblePrivateDetails describes private details of a marble
type MarblePrivateDetails struct {
ID string `json:"id"`
AppraisedValue int `json:"appraisedValue"`
}
// CreateMarble creates a new marble with private data
func (s *SmartContract) CreateMarble(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string) error {
// Check if marble already exists
exists, err := s.MarbleExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the marble %s already exists", id)
}
// Create marble object
marble := Marble{
ID: id,
Color: color,
Size: size,
Owner: owner,
}
// Convert to JSON
marbleJSON, err := json.Marshal(marble)
if err != nil {
return err
}
// Put marble in public data
err = ctx.GetStub().PutState(id, marbleJSON)
if err != nil {
return fmt.Errorf("failed to put marble in public data: %v", err)
}
// Get private data from transient map
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient: %v", err)
}
// Private data must be passed in the transient field
privateDataJSON, ok := transientMap["marble_private_details"]
if !ok {
return fmt.Errorf("marble_private_details key not found in the transient map")
}
var privateData MarblePrivateDetails
err = json.Unmarshal(privateDataJSON, &privateData)
if err != nil {
return fmt.Errorf("failed to unmarshal private details: %v", err)
}
// Verify that the private data ID matches the public data ID
if privateData.ID != id {
return fmt.Errorf("private data ID %s does not match public data ID %s", privateData.ID, id)
}
// Put private data in the private data collection
err = ctx.GetStub().PutPrivateData("collectionMarblePrivateDetails", id, privateDataJSON)
if err != nil {
return fmt.Errorf("failed to put private details: %v", err)
}
return nil
}
// ReadMarble returns the marble stored in the world state with given id
func (s *SmartContract) ReadMarble(ctx contractapi.TransactionContextInterface, id string) (*Marble, error) {
marbleJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if marbleJSON == nil {
return nil, fmt.Errorf("the marble %s does not exist", id)
}
var marble Marble
err = json.Unmarshal(marbleJSON, &marble)
if err != nil {
return nil, err
}
return &marble, nil
}
// ReadMarblePrivateDetails returns the private details of a marble
func (s *SmartContract) ReadMarblePrivateDetails(ctx contractapi.TransactionContextInterface, id string) (*MarblePrivateDetails, error) {
privateDataJSON, err := ctx.GetStub().GetPrivateData("collectionMarblePrivateDetails", id)
if err != nil {
return nil, fmt.Errorf("failed to read private details: %v", err)
}
if privateDataJSON == nil {
return nil, fmt.Errorf("the private details for %s do not exist", id)
}
var privateData MarblePrivateDetails
err = json.Unmarshal(privateDataJSON, &privateData)
if err != nil {
return nil, err
}
return &privateData, nil
}
// DeleteMarble deletes a marble from the world state
func (s *SmartContract) DeleteMarble(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.MarbleExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the marble %s does not exist", id)
}
// Delete the marble from the state
err = ctx.GetStub().DelState(id)
if err != nil {
return fmt.Errorf("failed to delete marble: %v", err)
}
// Delete private details of marble
err = ctx.GetStub().DelPrivateData("collectionMarblePrivateDetails", id)
if err != nil {
return fmt.Errorf("failed to delete private details: %v", err)
}
return nil
}
// MarbleExists returns true when marble with given ID exists in world state
func (s *SmartContract) MarbleExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
marbleJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return marbleJSON != nil, nil
}
func main() {
chaincode, err := contractapi.NewChaincode(&SmartContract{})
if err != nil {
fmt.Printf("Error creating marble chaincode: %v\n", err)
return
}
if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting marble chaincode: %v\n", err)
}
}
Using Private Data in Client Applications
// Example of using private data in a client application
const { Gateway, Wallets } = require('fabric-network');
const path = require('path');
const fs = require('fs');
async function main() {
try {
// Load the network configuration
const ccpPath = path.resolve(__dirname, 'connection-org1.json');
const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));
// Create a new file system based wallet for managing identities
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = await Wallets.newFileSystemWallet(walletPath);
// Check to see if we've already enrolled the user
const identity = await wallet.get('appUser');
if (!identity) {
console.log('An identity for the user "appUser" does not exist in the wallet');
console.log('Run the registerUser.js application before retrying');
return;
}
// Create a new gateway for connecting to our peer node
const gateway = new Gateway();
await gateway.connect(ccp, {
wallet,
identity: 'appUser',
discovery: { enabled: true, asLocalhost: true }
});
// Get the network (channel) our contract is deployed to
const network = await gateway.getNetwork('mychannel');
// Get the contract from the network
const contract = network.getContract('marblecontract');
// Create a new marble with private data
const marbleId = `marble${Math.floor(Math.random() * 10000)}`;
const privateData = {
id: marbleId,
appraisedValue: 1000
};
console.log(`Creating marble ${marbleId} with private data`);
await contract.submitTransaction(
'CreateMarble',
marbleId,
'blue',
'10',
'tom',
Buffer.from(JSON.stringify(privateData)).toString('base64')
);
console.log('Transaction has been submitted');
// Read the public data
console.log(`Reading public data for marble ${marbleId}`);
const publicData = await contract.evaluateTransaction('ReadMarble', marbleId);
console.log(`Public data: ${publicData.toString()}`);
// Read the private data
console.log(`Reading private data for marble ${marbleId}`);
const privateDataResult = await contract.evaluateTransaction('ReadMarblePrivateDetails', marbleId);
console.log(`Private data: ${privateDataResult.toString()}`);
// Disconnect from the gateway
await gateway.disconnect();
} catch (error) {
console.error(`Failed to submit transaction: ${error}`);
process.exit(1);
}
}
main();
Zero-Knowledge Proofs
Zero-Knowledge Proofs (ZKPs) allow one party to prove to another that a statement is true without revealing any additional information.
Implementing ZKPs with zk-SNARKs
// Example of using zk-SNARKs with Fabric
const { Gateway, Wallets } = require('fabric-network');
const snarkjs = require('snarkjs');
const fs = require('fs');
const path = require('path');
// Generate a proof that a number is in a specific range without revealing the number
async function generateRangeProof(number, lowerBound, upperBound) {
// Load the circuit
const circuit = JSON.parse(fs.readFileSync('./circuits/range_proof.json'));
// Generate witness
const input = {
"number": number,
"lowerBound": lowerBound,
"upperBound": upperBound
};
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
"./circuits/range_proof.wasm",
"./circuits/range_proof_final.zkey"
);
// Convert the proof to a format that can be verified on-chain
const proofFormatted = {
a: [proof.pi_a[0], proof.pi_a[1]],
b: [[proof.pi_b[0][0], proof.pi_b[0][1]], [proof.pi_b[1][0], proof.pi_b[1][1]]],
c: [proof.pi_c[0], proof.pi_c[1]]
};
return {
proof: proofFormatted,
publicSignals: publicSignals
};
}
// Verify a range proof
async function verifyRangeProof(proof, publicSignals) {
const vKey = JSON.parse(fs.readFileSync("./circuits/verification_key.json"));
const res = await snarkjs.groth16.verify(vKey, publicSignals, proof);
return res;
}
// Submit a transaction with a zero-knowledge proof
async function submitTransactionWithZKP(assetId, value) {
try {
// Generate a proof that the value is between 0 and 1000
const { proof, publicSignals } = await generateRangeProof(value, 0, 1000);
// Verify the proof locally first
const isValid = await verifyRangeProof(proof, publicSignals);
if (!isValid) {
throw new Error("Invalid proof generated");
}
// Connect to Fabric
const ccpPath = path.resolve(__dirname, 'connection-org1.json');
const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = await Wallets.newFileSystemWallet(walletPath);
const identity = await wallet.get('appUser');
if (!identity) {
throw new Error('User identity not found in wallet');
}
const gateway = new Gateway();
await gateway.connect(ccp, {
wallet,
identity: 'appUser',
discovery: { enabled: true, asLocalhost: true }
});
const network = await gateway.getNetwork('mychannel');
const contract = network.getContract('zkpcontract');
// Submit the transaction with the proof
await contract.submitTransaction(
'CreateAssetWithZKP',
assetId,
JSON.stringify(proof),
JSON.stringify(publicSignals)
);
console.log(`Asset ${assetId} created with ZKP verification`);
gateway.disconnect();
} catch (error) {
console.error(`Failed to submit transaction with ZKP: ${error}`);
throw error;
}
}
// Example usage
submitTransactionWithZKP('asset123', 500);
ZKP Chaincode Implementation
// Example of chaincode that verifies zero-knowledge proofs
package main
import (
"encoding/json"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// SmartContract provides functions for ZKP verification
type SmartContract struct {
contractapi.Contract
}
// Asset represents a general asset that is managed by the chaincode
type Asset struct {
ID string `json:"id"`
RangeProofID string `json:"rangeProofId"`
Owner string `json:"owner"`
}
// RangeProof represents a zero-knowledge proof that a value is in a specific range
type RangeProof struct {
ID string `json:"id"`
Proof string `json:"proof"`
PublicSignals string `json:"publicSignals"`
Verified bool `json:"verified"`
}
// CreateAssetWithZKP creates a new asset with a zero-knowledge proof
func (s *SmartContract) CreateAssetWithZKP(ctx contractapi.TransactionContextInterface, id string, proofJSON string, publicSignalsJSON string) error {
// Check if asset already exists
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
// Verify the proof (in a real implementation, this would call a ZKP verification library)
// For this example, we'll just store the proof and mark it as verified
proofID := fmt.Sprintf("%s_proof", id)
rangeProof := RangeProof{
ID: proofID,
Proof: proofJSON,
PublicSignals: publicSignalsJSON,
Verified: true, // In a real implementation, this would be the result of verification
}
rangeProofJSON, err := json.Marshal(rangeProof)
if err != nil {
return err
}
err = ctx.GetStub().PutState(proofID, rangeProofJSON)
if err != nil {
return fmt.Errorf("failed to put range proof in world state: %v", err)
}
// Get the client identity
clientID, err := s.getSubmittingClientIdentity(ctx)
if err != nil {
return err
}
// Create the asset
asset := Asset{
ID: id,
RangeProofID: proofID,
Owner: clientID,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
err = ctx.GetStub().PutState(id, assetJSON)
if err != nil {
return fmt.Errorf("failed to put asset in world state: %v", err)
}
return nil
}
// ReadAsset returns the asset stored in the world state with given id
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// ReadRangeProof returns the range proof stored in the world state with given id
func (s *SmartContract) ReadRangeProof(ctx contractapi.TransactionContextInterface, id string) (*RangeProof, error) {
proofJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if proofJSON == nil {
return nil, fmt.Errorf("the range proof %s does not exist", id)
}
var proof RangeProof
err = json.Unmarshal(proofJSON, &proof)
if err != nil {
return nil, err
}
return &proof, nil
}
// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return assetJSON != nil, nil
}
// Helper function to get the submitting client identity
func (s *SmartContract) getSubmittingClientIdentity(ctx contractapi.TransactionContextInterface) (string, error) {
clientID, err := ctx.GetClientIdentity().GetID()
if err != nil {
return "", fmt.Errorf("failed to get client identity: %v", err)
}
return clientID, nil
}
func main() {
chaincode, err := contractapi.NewChaincode(&SmartContract{})
if err != nil {
fmt.Printf("Error creating zkp chaincode: %v\n", err)
return
}
if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting zkp chaincode: %v\n", err)
}
}
Secure Chaincode Development
Developing secure chaincode is critical for maintaining the integrity and security of your blockchain network.
Input Validation
// Example of input validation in chaincode
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, value int) error {
// Validate input parameters
if len(id) == 0 {
return fmt.Errorf("asset ID cannot be empty")
}
if len(id) > 128 {
return fmt.Errorf("asset ID cannot exceed 128 characters")
}
if len(color) == 0 {
return fmt.Errorf("color cannot be empty")
}
if size <= 0 {
return fmt.Errorf("size must be a positive integer")
}
if len(owner) == 0 {
return fmt.Errorf("owner cannot be empty")
}
if value < 0 {
return fmt.Errorf("value cannot be negative")
}
// Check if asset already exists
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
// Create asset
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
Value: value,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
Access Control
// Example of access control in chaincode
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
// Get the asset
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
// Get the client identity
clientID, err := ctx.GetClientIdentity().GetID()
if err != nil {
return fmt.Errorf("failed to get client identity: %v", err)
}
// Check if the client is the owner of the asset
clientIDHash, err := ctx.GetClientIdentity().GetID()
if err != nil {
return fmt.Errorf("failed to get client identity: %v", err)
}
// In a real implementation, you would need to extract the actual identity from the certificate
// and compare it with the asset owner. This is a simplified example.
if asset.Owner != clientIDHash {
return fmt.Errorf("client %s is not the owner of the asset", clientID)
}
// Transfer the asset
asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
Secure Coding Practices
- Avoid Common Vulnerabilities:
- SQL Injection: Use parameterized queries when interacting with external databases
- Cross-Site Scripting (XSS): Sanitize input and output in client applications
-
Command Injection: Avoid using user input in system commands
-
Error Handling:
- Provide meaningful error messages for debugging
- Avoid exposing sensitive information in error messages
-
Implement proper logging
-
Secure Dependencies:
- Use dependency scanning tools to identify vulnerabilities
- Keep dependencies up to date
-
Use a minimal set of dependencies
-
Code Review:
- Implement a peer review process
- Use static code analysis tools
- Conduct regular security audits
Hardware Security Modules (HSMs)
Hardware Security Modules (HSMs) provide secure key management and cryptographic operations.
Configuring Fabric to Use HSMs
# core.yaml
peer:
BCCSP:
Default: PKCS11
PKCS11:
Library: /usr/local/lib/softhsm/libsofthsm2.so
Label: ForFabric
Pin: 98765432
Hash: SHA2
Security: 256
Using HSMs with Fabric CA
# fabric-ca-server-config.yaml
bccsp:
default: PKCS11
pkcs11:
library: /usr/local/lib/softhsm/libsofthsm2.so
label: ForFabric
pin: 98765432
hash: SHA2
security: 256
Implementing HSM Support in Applications
// Example of using HSMs in a Node.js application
const { Gateway, Wallets } = require('fabric-network');
const fs = require('fs');
const path = require('path');
const pkcs11js = require('pkcs11js');
// Initialize PKCS#11 library
const pkcs11 = new pkcs11js.PKCS11();
pkcs11.load('/usr/local/lib/softhsm/libsofthsm2.so');
pkcs11.C_Initialize();
// Get slot and session
const slots = pkcs11.C_GetSlotList(true);
const session = pkcs11.C_OpenSession(slots[0], pkcs11js.CKF_RW_SESSION | pkcs11js.CKF_SERIAL_SESSION);
pkcs11.C_Login(session, pkcs11js.CKU_USER, '98765432');
// Function to sign data using HSM
function signWithHSM(data, keyLabel) {
// Find private key
const privateKeyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PRIVATE_KEY },
{ type: pkcs11js.CKA_LABEL, value: keyLabel }
];
const privateKey = pkcs11.C_FindObjects(session, privateKeyTemplate)[0];
// Sign data
pkcs11.C_SignInit(session, { mechanism: pkcs11js.CKM_ECDSA }, privateKey);
const signature = pkcs11.C_Sign(session, data);
return signature;
}
// Connect to Fabric using HSM for signing
async function connectToFabricWithHSM() {
try {
// Load the network configuration
const ccpPath = path.resolve(__dirname, 'connection-org1.json');
const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));
// Create a custom identity provider that uses HSM for signing
const hsmIdentityProvider = {
getUserContext: async (name, password) => {
// In a real implementation, you would retrieve the user's certificate
// and use the HSM to sign transactions
const certificate = fs.readFileSync(`./certificates/${name}.pem`);
return {
name,
certificate,
sign: async (digest) => {
return signWithHSM(digest, `${name}_key`);
}
};
}
};
// Create a new wallet with the HSM identity provider
const wallet = await Wallets.newInMemoryWallet();
await wallet.put('hsmUser', hsmIdentityProvider);
// Connect to Fabric
const gateway = new Gateway();
await gateway.connect(ccp, {
wallet,
identity: 'hsmUser',
discovery: { enabled: true, asLocalhost: true }
});
// Get the network and contract
const network = await gateway.getNetwork('mychannel');
const contract = network.getContract('assetcontract');
// Use the contract
const result = await contract.evaluateTransaction('GetAllAssets');
console.log(`Result: ${result.toString()}`);
// Disconnect from the gateway
await gateway.disconnect();
} catch (error) {
console.error(`Failed to connect to Fabric with HSM: ${error}`);
} finally {
// Clean up PKCS#11 resources
pkcs11.C_Logout(session);
pkcs11.C_CloseSession(session);
pkcs11.C_Finalize();
}
}
connectToFabricWithHSM();
Secure Channel Configuration
Properly configuring channels is essential for maintaining security in a Hyperledger Fabric network.
Channel Policies
# configtx.yaml
Policies:
Readers:
Type: ImplicitMeta
Rule: "ANY Readers"
Writers:
Type: ImplicitMeta
Rule: "ANY Writers"
Admins:
Type: ImplicitMeta
Rule: "MAJORITY Admins"
BlockValidation:
Type: ImplicitMeta
Rule: "ANY Writers"
Endorsement Policies
# Chaincode endorsement policy
Endorsement:
Type: Signature
Rule: "OR('Org1MSP.peer', 'Org2MSP.peer')"
Updating Channel Policies
# Fetch the current channel configuration
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c mychannel --tls --cafile $ORDERER_CA
# Convert the configuration to JSON
configtxlator proto_decode --input config_block.pb --type common.Block | jq .data.data[0].payload.data.config > config.json
# Modify the policy
jq '.channel_group.groups.Application.policies.Endorsement.policy.value.rule = "MAJORITY Endorsement"' config.json > modified_config.json
# Convert back to protobuf
configtxlator proto_encode --input config.json --type common.Config --output config.pb
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
# Calculate the update
configtxlator compute_update --channel_id mychannel --original config.pb --updated modified_config.pb --output config_update.pb
# Convert the update to JSON
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate | jq . > config_update.json
# Wrap the update in an envelope
echo '{"payload":{"header":{"channel_header":{"channel_id":"mychannel", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json
# Convert the envelope to protobuf
configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb
# Sign and submit the update
peer channel update -f config_update_in_envelope.pb -c mychannel -o orderer.example.com:7050 --tls --cafile $ORDERER_CA
Security Best Practices
When implementing advanced security features in Hyperledger Fabric, follow these best practices:
- Network Security:
- Use TLS for all communications
- Implement proper firewall rules
- Use network segmentation
-
Regularly update and patch all components
-
Identity Management:
- Use a robust identity management system
- Implement certificate rotation
- Revoke compromised certificates
-
Use HSMs for key management
-
Access Control:
- Implement the principle of least privilege
- Use fine-grained access control policies
- Regularly review and audit access
-
Implement multi-signature requirements for critical operations
-
Data Protection:
- Use private data collections for sensitive information
- Implement data encryption at rest and in transit
- Consider using zero-knowledge proofs for privacy
-
Implement proper data backup and recovery procedures
-
Monitoring and Auditing:
- Implement comprehensive logging
- Set up real-time monitoring
- Conduct regular security audits
- Implement intrusion detection systems
By implementing these advanced security features and following best practices, you can create a secure Hyperledger Fabric network that protects sensitive data and maintains the integrity of your blockchain solution.