An Implementation Example of Notarization in Hyperledger Fabric
Overview
Notarization is always considered as a good use case in Distributed Ledger Technology (DLT). It is largely due to the nature of data immutability, transaction audibility and decentralization in DLTs.
As mentioned in my previous works (link), there are different types of notarization. This work is an application about proof-of-ownership: to show a document owned by someone is recorded in ledger using digital signature technology.
Theoretically this type of application can run on any DLT setup, be it permissionless and permissioned one. In fact I have developed one in Ethereum Rinkeby testnet. However in this article the application is built on top of Hyperledger Fabric, a permissioned blockchain platform. I will talk about these two platforms later.
This article provides some in-depth discussion on the chaincode and client applications. All the codes and demonstration flow are provided for easy hands-on practice.
Design
Ownership of a Document by Digital Signature
As said, the mechanism of proving ownership is through digital signature in public key cryptography. Each individual is given a key pair, designated as a private key and a public key. These are mathematically related. Anything encrypted with private key can only be decrypted by the corresponding public key. In digital signature use, the document-to-be-validated is first “hashed” to a fixed length (called hash or digest) and encrypted with one’s private key. The process is called signing, and the result is called one’s signature on that document-to-be-validated.
For anyone who wishes to verify the signature, the document-to-be-validated, the signature, and the public key of the owner is needed. The signature is first decrypted with the public key. The result is compared to the hash of document-to-be-validated. If identical, anyone can conclude that the document is signed by someone who owns the private key, corresponding to the public key being used.
There is a small caveat here: I simply equate “one signs a document” to “one owns a document”. It may be possible in some cases but not in others. In real life further process may be needed to enhance the ownership. Again, for sake of demonstration, this is well assumed.
Identity in Ethereum and Hyperledger Fabric
Theoretically this type of application and the digital signature can be done almost in all types of blockchain platform. In fact I also have developed this similar application on Ethereum Rinkeby almost a year ago (and it is still running).
However, being a permissionless blockchain, Ethereum has no implicit way to link the holding of private/public key to one’s identity. I am taking a tedious approach by adding a layer of Public Key Infrastructure (PKI), using X509 certificate to identify individual, and document signing and verification is done on X509 certificate instead of native Ethereum network.
Hyperledger Fabric as a permissioned blockchain requires anything (and anyone) is identified before using the network. And good enough they are using X509 certificates for identification. Therefore, the same set of certificate is used for both signing transaction in fabric network, and signing document for application.
Using permissioned blockchain like Hyperledger Fabric brings in both good sides and bad sides. On one side the identify mechanism is already there, simplifying the design. On the other hand, one has to build the fabric network and after that maintain it, and the level of distribution is far lower than a permissionless blockchain network like Ethereum. Nevertheless, both cases rely on centralized element (the Certificate Authority, CA) and therefore trust-level wise, they are the same.
Overall Flow
So the design of application looks like this. Application user is first registered and enrolled into the CA. After enrollment the user is given a signing key (i.e. private key) and the certificate, which contains user’s identity, user’s public key, and CA’s signature on the certificate. This information is stored in user’s wallet.
When a user (originator) needs to record the ownership of a document, the application computes the document hash, and the signature using user’s private key. These two pieces of information is submitted to the fabric network. The ledger in fabric network will record the signature and timestamp of submission, with the document hash as key.
When anyone needs to verify that ownership of that document, the information needed is the document itself and the originator’s certificate. The application will compute the hash of the document and retrieve the record from the ledger based on the hash. If found, the signature and the timestamp is retrieved. Then the application performs the validation by using the given signature and public key in the certificate. After validation, one can be sure that this document has been signed by the originator, and the record is found in the ledger.
The validation fails when one is using someone’s else certificate (not the originator) or that document has not been recorded before.
Application Deep Dive
Application in Hyperledger Fabric is mainly composed of two portions: the chaincode (the “smart contract” being executed in endorsing peers in the fabric network), and the client applications (the client portion interacting with the chaincode using software development kit (SDK). Here we will take a look on both.
Chaincode
The chaincode is responsible for interacting with the ledger. In our example, we largely adapt the chaincode of fabcar example inside fabric-samples. Here I just highlight those relevant portions in the chaincode.
Data Structure in Ledger
As the purpose of our application is to keep the signature and timestamp of a document submitted by a user, we use a rather simple data structure to keep this.
type DocRecord struct {
Signature string `json:"signature"`
Time string `json:"time"`
}
Chaincode Functions
Following the best practice, we do not define the Init(). And inside Invoke(), we define two functions: queryDocRecord() and createDocRecord().
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
return shim.Success(nil)
}func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {// Retrieve the requested Smart Contract function and arguments
function, args := APIstub.GetFunctionAndParameters()
// Route to the appropriate handler function to interact with the ledger appropriately
if function == "queryDocRecord" {
return s.queryDocRecord(APIstub, args)
} else if function == "createDocRecord" {
return s.createDocRecord(APIstub, args)
}return shim.Error("Invalid Smart Contract function name.")
}
In createDocRecord(), we are expecting two parameters, the hash of a document and the document signature by the owner (the computation of hash and signature are outside the chaincode). When recorded in the ledger, the hash is the key, while the document signature and timestamp is the value of this key.
func (s *SmartContract) createDocRecord(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}var docrecord = DocRecord{Signature: args[1], Time: time.Now().Format(time.RFC3339)}
docrecordAsBytes, _ := json.Marshal(docrecord)
APIstub.PutState(args[0], docrecordAsBytes)return shim.Success(nil)
}
In queryDocRecord(), we are expecting one parameter, which is the hash of a document. With this hash as key, the record in the ledger is retrieved.
func (s *SmartContract) queryDocRecord(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}docrecordAsBytes, _ := APIstub.GetState(args[0])if docrecordAsBytes == nil {
return shim.Error("Document not found: " + args[0])
}return shim.Success(docrecordAsBytes)
}
Client Application
Now it is the client application. We totally have four client applications for this application.
enrollAdmin.js
This one is directly taken from fabcar example without modification.
The purpose is to enrol an admin user to the Fabric CA of Basic Network. This admin will later register user for our demo.
After enrollment, the admin (and other users as well) will have a directory created inside docrec/wallet directory. Inside the wallet there are signing key and X509 certificate issued by Fabric CA. They will be used on other client applications.
If admin already exists in the docrec/wallet, no further action is needed.
Meanwhile, per the Basic Network setup, the Fabric CA will have all the things “hardcoded”. As long as we do not modify the crypto detail on Basic Network, we can enrol the admin once, and use the admin wallet for the whole demo.
$ node enrollAdmin.js
registerUser.js
The application is mainly adapted from fabcar example with modification. The original fabcar example creates a wallet for ‘user1’ (hardcoded). Since we need multiple users, we will add a username when using this application.
The purpose is to register a user by admin, and then a user can enroll the Fabric CA, that is, collect the signing key and X509 certificate. Like admin user, each user will have its own wallet, stored under docrec/wallet directory.
Similarly, as long as Basic Network setup is not modified, we can generate once these users and use for demo any time after.
$ node registerUser.js <username>// examples
$ node registerUser.js alice
$ node registerUser.js bob
addDocByFile.js
The purpose of this application is to record the document in this notarization application.
The input parameters are
- user: From the wallet of user user’s signing key is retrieved. This signing key is to generate siganture on the hash of the file specified.
- filename: The file to be recorded. The hash (SHA-256) is computed.
After computation, it invokes chaincode function createDocRecord(), which requires the information of
- hash of file
- signature of the hash by user
$ node addDocByFile.js <user> <filename>// example
$ node addDocByFile.js alice alicefile
$ node addDocByFile.js bob bobfile
validateDocByFile.js
The purpose of this application is to validate the document in this notarization application. The validation can help to prove that,
- the document has been recorded in the ledger before (otherwise, document not found is responded)
- the signature is correct and created by the certificate provided (otherwise, signature validation fails)
- the certificate provided is a valid certificate by a CA
- the time when the record is created
The input parameters are
- user: Here user can be anyone with a valid wallet. Consider validation can be done by anyone, not just the document owner or admin. However, we still need user as Hyperledger Fabric as a permissioned blockchain requires identity.
- filename: The file to be validated. The hash (SHA-256) is computed from the file.
- usercert: The X509 certificate of the file owner. One’s certificate should be public available and the subject is the identify of the certificate owner.
$ node addDocByFile.js <any_user> <filename> <usercert>// example
$ node validateDocByFile.js alice alicefile alicecert
$ node validateDocByFile.js admin bobfile bobcert
Codes
Here are the codes of chaincode and several client applications.
Demonstration
The demo setup is built on Basic Network from fabric-samples. It is a single-organization single-peer setup with one order and one Fabric CA. Commands are issued through CLI. It is good enough to show how the application works.
On this Basic Network a channel mychannel is created, and the peer joins the channel. After that the chaincode docrec.go is installed and instantiated.
After that client applications are used to interact with the chaincode.
Step 1: Create Project Directory
Create a project directory under fabric-samples. (The demo code contains relative directory structure. Therefore make sure it is on the right directory.)
# inside fabric-samples
$ mkdir docrec && cd docrec
Step 2: Load the Required Packages
Besides Fabric SDK, we also needs handling X509 certificate and cryptography.
# inside fabric-samples/docrec
$ npm init -y
$ npm install fabric-ca-client fabric-network crypto-js jsrsasign -S
Step 3: Chaincode
For demo purpose, we place the chaincode under fabric-samples/chaincode directory. This directory is accessible in the running containers of basic-network.
# inside fabric-samples/chaincode
$ mkdir docrec && cd docrec
$ <put the docrec.go file here>
After we should be able to see the chaincode file in this directory.
Step 4: Client Application
There are total four client applications to be used. We simply put them in fabric-samples/docrec directory.
# inside fabric-samples/docrec
$ <put the four files enrollAdmin.js, registerUser.js, addDocByFile.js and validateDocByFile.js>
After we should be able to see the four client application files in this directory.
Step 5: Bring Up Basic Network
Bring up Basic Network and check if all five containers are up and running. After that the Basic Network is up and running with a channel mychannel is created and the peer node joins the channel.
# inside fabric-samples/basic-network
$ ./start.sh && docker-compose -f docker-compose.yml up -d cli
$ docker ps
Check all five containers are up and running.
Step 6: Install and Instantiate docrec Chaincode
After Basic Network is up and running, we can work on the chaincode installation and instantiation.
# any directory
$ docker exec cli peer chaincode install -n docrec -v 1.0 -p "github.com/docrec"
$ docker exec cli peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n docrec -v 1.0 -c '{"Args":[]}' -P "OR ('Org1MSP.member')"
Step 7: Enrol admin as Admin (if not yet)
Use enrollAdmin.js to enrol an admin. If admin is in docrec/wallet, it is already enrolled and it will be used later.
# inside fabric-samples/docrec
$ node enrollAdmin.js
$ ls wallet
Step 8: Register Users alice and bob, and Enrol them as Users (if not yet)
Use registerUser.js to enrol users alice and bob. If they are already in the docrec/wallet, they are already enrolled and can be used later.
# inside fabric-samples/docrec
$ node registerUser.js alice
$ node registerUser.js bob
$ ls wallet
Step 9: Extract Certificate from alice and bob
We later needs the certificate from alice and bob for signature validation. The certificate can be found inside their wallets. The directory is docrec/wallets/alice/alice.
# inside fabric-samples/docrec
$ cat wallet/alice/alice
$ cat wallet/bob/bob
Use any editor to extract them and keep them into two files: alicecert and bobcert.
Here is how alicecert looks like.
Step 10: Create two files for demonstration
Create two files for demonstration.
# inside fabric-sample/docrec
$ echo "This is a test file for alice" > alicefile
$ echo "This is another test file for bob" > bobfile
Step 11: Add Files in the Notarization Application
Now we have all files ready for demonstration. We will use addDocByFile.js to add alicefile by alice, and bobfile by bob.
# inside fabric-sample/docrec
$ node addDocByFile.js alice alicefile
$ node addDocByFile.js bob bobfile
From the response we see the transaction is successfully submitted.
Step 12: Validate Files in the Notarization Application
We can use validateDocByFile.js to validate files with the certificates.
We first validate alicefile with alicecert, and bobfile with bobcert. Both should work. Meanwhile, we need to specify the user as someone issues this query, but it is not relevant in the validation process. As an example, we just use alice as the query issuer.
# inside fabric-sample/docrec
$ node validateDocByFile.js alice alicefile alicecert
$ node validateDocByFile.js alice bobfile bobcert
In both case we see positive result (true).
Next we validate with wrong certificate.
# inside fabric-sample/docrec
$ node validateDocByFile.js alice alicefile bobcert
$ node validateDocByFile.js alice bobfile alicecert
And we see negative result.
Meanwhile, some detail of certificate provided is given in each validation. We can further consider whether this certificate is acceptable or not (e.g. whether it is from a trusted issuer, whether the subject names are correct, whether the certificate has expired, etc.)
Finally we validate a file that is not submitted before.
# inside fabric-sample/docrec
$ echo "File not recorded" > nofile
$ node validateDocByFile.js alice nofile alicecert
Document not found in this query.
Step 13: Teardown the Demonstration
After demonstration, we will tear down the whole Basic Network setup. The scripts stop and remove containers and image created during the demonstration.
Note all file record in the ledger is removed. The wallets are still there, and next time when perform demonstration, they don’t need created again (i.e. Step 7 and 8 can be skipped).
Closing
In this exercise we have built a notarization application for recording the ownership of documents on a fabric network. Although the application is demonstrated in Basic Network, it should work fine with a proper setup of a fabric network.
This setup is for demonstration purpose and therefore the applications are not optimized and no user interface is developed. Nevertheless, it shows how to leverage the identity management with X509 for individual user as ownership of any document. It can be further evolved to become a useable application for a larger population, such as a user interface to register and enrol the CA and collect the certificate in their own wallet, select their wallet for signing document, etc.