A major challenge in many blockchain solutions is to securely connect physical actors or objects to their digital representation in the blockchain. Some obvious examples are supply chain and anti-counterfeiting solutions, where digital information pertaining to the production, origin, chain of custody, or shipping conditions is immutably secured in the blockchain. In most solutions, public/private key pairs representing digital and physical actors, machines, or goods are remotely managed in remote or cloud server back-ends. As ownership, identification, and trust in blockchain solutions are conveyed through the control of private keys, remote key management systems pose an expensive and laborious affair at best, and a serious at worst. Thus, moving the key management process closer to the objects of interest not only saves valuable resources; it adds a significant layer of trust to the solution. security risk In this article, we describe how smart card NFC chips with blockchain capacities can be used for more secure key management in blockchain solutions, and specifically in solutions built with the Lisk SDK and JavaScript. Firstly, we briefly describe the technology that Gimly uses for this purpose. Secondly, we describe the Smart Transport proof-of-concept that was built by Marc Buma and Caspar Roelofs and that was funded through the Lisk Builders program. Finally, we dive a little bit deeper into the code, to show in more detail how the microchips interact with the Lisk blockchain in our JavaScript code base. Tangem by Gimly: made for blockchain NFC chips Gimly works with as a supplier and implementation partner, to provide a new approach towards more secure management of public/private key pairs in blockchain solutions. Tangem’s made-for-blockchain NFC smartcard chips are EAL6+ certified, containing a secure element where the keypairs are newly generated, and from which the private key cannot be extracted, exposed, or copied. Tangem The chip’s firmware enables the use of these self-contained keys to sign blockchain transactions directly from the chip, and to sign, store, and present encrypted data using its secure storage. Chip operations are protected by means of various security options (including 2FA, passcodes, and multiple PIN options), and the firmware is independently audited by Kudelski security firm. The chips are available in various form-factors, including durable physical credit card format, adhesive stickers, or tags. The physical card format is currently available as pre-provisioned crypto-notes for all major cryptocurrencies, or as developer cards that can be provisioned to work with any blockchain that uses either secp256 or ed25519 cryptographic curves, including Lisk. here Lisk Builders: Smart Transport PoC The Smart Transport PoC uses a Tangem smartcard to demonstrate how a hardware wallet can be used to secure a package using the Lisk SDK. In this demonstration, a new client application has been developed for the Lisk Transport demo that uses an NFC reader to communicate with a Tangem smartcard. In the demo, we assume that the smartcard is part of a smart container, i.e. a container secured with its own hardware wallet that is used to sign all measurements of the temperature and alarm conditions that the container reports. During the transport, all parties connected to the transport, (client, supplier, and recipient) can determine the public key for the smart container and thus verify that the received data actually originates from this transport. When the smart container is prepared for a new transport, a new account is created on the smartcard that is unique for this transport. This account is automatically funded with some LSK in order to be able to autonomously submit transactions. While the transport is ongoing, all transactions that originate from the smart container (measurements, alarms) are signed by this account, thus proving their origin and authenticity. Once the transport is finished, surplus LSK from the account is returned and the account is erased. Now the smart container can be used for a new transport. The demo adds the following elements to the Lisk Transport example: A driver for PC/SC compatible smartcard readers. In our demonstration, we use an ACS ACR1252 reader. This driver is used to poll the smartcard reader. A driver for the Tangem protocol that implements all functions that are required to make the steps above possible (create wallet, purge wallet, sign transaction hashes using the smartcard). Implementation of the register-measurement custom transaction. A new client application that is used to start and finish the transport and that can be used to simulate measurements. A new IoT application that uses the smartcard reader to sign simulated temperature measurements. Visualisation of temperature data stored on the blockchain during the transport. Hands-on with the demo Since this demo builds on the Lisk Transport workshop, you should at least proceed to step 0 (Workshop Part 0: ) before starting. At this point, you should have a running blockchain with all prerequisites installed. Installation and Setup In addition, you should make sure that the proper PC/SC drivers for your NFC reader have been installed. Now you can clone the sources of the demo to a folder on your hard disk. The repository can be found . here The project root for the next steps is located in the folder. gimly-transport The demonstration will use 3 terminal windows: First window running the blockchain Navigate to the project root Install the node dependencies in the transactions folder ○ cd transactions ○ npm install Install the node dependencies in the tangem-smart folder ○ cd ../tangem-smart ○ npm install Next, navigate into the node folder and start the blockchain ○ cd ../node ○ npm install ○ node index.js | npx bunyan -o short Second window running the client application navigate to the project root ○ cd client ○ npm install ○ node app.js Third window running the IoT application navigate to the project root ○ cd iot/temperature_and_humidity ○ npm install ○ node index.js Now you can connect to your laptop to view the client application by opening in your web browser. http://localhost:3000 Step 1: Place the smartcard on the NFC reader Select one of the smartcards and put it on the reader. Now press refresh. The application will recognize the card and allow you to create a new shipment. If the smartcard is already in use, it will show the status of the current transport (step 3, 4, or 5, depending on the transport status). Step 2: Create shipment First, select one of the demo accounts as the sender for this package. Ensure that the sender has sufficient funds, otherwise, the creation of the shipment will fail (you can use the fund button to acquire some LSK from the faucet). Next, select the recipient and set the transport conditions (postage and security). Keep the trust level at 0 for your first transport. As you simulate more transport, the trust level of the employed carrier will increase. Finally, press create to create the shipment. Keep an eye on the terminal window for the client to see what is happening: First, a new wallet account is created on the smartcard. Next, the wallet receives some funds from the faucet with a transfer transaction. Finally, the new shipment is registered on the blockchain with a register-packet transaction. When these actions are completed, the browser will notify you: Press back to return to the main page. Step 3: Start transport First, make sure that the carrier has sufficient funds to pay the security (you can use the fund button to acquire some LSK from the faucet). Next, press “start transport”. A start-transport transaction is now sent to the blockchain. When the transaction has been sent, the browser will notify you. Press back to return to the main page. Step 4: Track transport and add measurements While the transport is ongoing, the IoTprocess in the separate window will start adding simulated temperature measurements to the shipment every 15 seconds. Look at the output in the IoT terminal window and the blockchain window to check. Before committing a measurement to the blockchain, a hash of the measurement data record is calculated and sent to the smartcard. The smartcard uses its private key to sign the transaction. Next, the signature is added to the transaction and the transaction is committed to the blockchain. When you press the yellow refresh button in the browser window, you can see the measurements appear in the graph. Step 5: Finish transport Finally, the recipient can select an end status for the shipment and finish the transport. Step 6: Reset smart container Once the shipment is finished, you can reset the smart package. Now the funds remaining in the smart wallet account are sent back to the faucet and the account is erased from the card. You can now re-use the card for the next shipment. Technical deep dive: looking at the code Starting point Below, the program code from the original Lisk Transport example is shown for a transaction that is processed using the Lisk SDK. It is part of the application that runs on the sensor module. After detecting that the package has been opened, the sensor module stores a new alarm transaction on the blockchain. api.accounts.get({ : packetCredentials.address }).then( { tx = LightAlarmTransaction({ : { : ().getTime() / }, : transactions.utils.convertLSKToBeddows( ), : response1.data[ ].nonce }); tx.sign(networkIdentifier, packetCredentials.passphrase); api.transactions.broadcast(tx.toJSON()).then( { .log( ); }).catch( { .dir(err); }); }); address => response1 let new asset timestamp new Date 1000 fee '0.01' nonce 0 => res console "transaction submitted" => err console In this code example, a new is created (#27), signed using the transaction’s sign method (#35), and then broadcast to the network (#37). Signing is done using a passphrase that is stored in local memory. LightAlarmTransaction In the transport demo, the account that signs the transaction is created using the client app and subsequently hardcoded in the program code. packetCredentials In our version of the project, credential management is transferred to the smartcard. This makes the key management more secure and adds an extra level of provenance to the data committed to the blockchain. Since the smartcard is an external device, some means of communication are required. In this case, messages to the card are sent using an NFC radio connection. In this post, we focus on the blockchain side of the code, but if you are interested in the inner workings of NFC communication using a USB NFC card reader, and the implementation of the required methods of the tangem protocol, you can find these in the . In the tangemcard.js module, three important methods have been implemented: tangem-smart subfolder : this method creates a new generic wallet account on the smartcard. The public key from this account is used to generate an address on the Lisk transport sidechain. createWallet : this method is used to sign a hash or to hash and sign a packet of binary data using the private key stored on the smartcard. signData : this method permanently erases the wallet account stored on the smartcard. purgeWallet Creating a wallet on the smartcard Before the smartcard can be used for transaction signing, a new wallet needs to be created on the smartcard and some funds need to be sent to this wallet. Once this is done, the address for the wallet needs to be registered as a valid package account on the blockchain. The process starts (see Step 2: Create shipment above) when the user places a blank smartcard (i.e. a smartcard without a wallet) near the NFC reader, it fills in the transport conditions, and presses the create button. This causes a function call on the function. The code for this function is found in the handlers.js file in the client folder. handlerRegisterSmartPackage handlerRegisterSmartPackage = (req, res) => { app.locals.formdata.passphrasesender = req.body.passphrasesender; app.locals.formdata.passphraserecipient = req.body.passphraserecipient; app.locals.formdata.postage = req.body.postage; app.locals.formdata.security = req.body.security; app.locals.formdata.minTrust = req.body.minTrust; (req.body.submit=== ) { participantAddress = getAddressForPassphrase(app.locals.formdata.passphrasesender); success = fundAddress(api, participantAddress, ); (success) { doInfoMessage(res, ) } { doErrorMessage(res, ); } } (req.body.submit=== ) { participantAddress = getAddressForPassphrase(app.locals.formdata.passphrasecarrier); success = fundAddress(api, participantAddress, ); (success) { doInfoMessage(res, ) } { doErrorMessage(res, ); } } { { (haveCard && currentPacketId !== ) { doErrorMessage(res, ); } passphrasesender = app.locals.formdata.passphrasesender; passphraserecipient = app.locals.formdata.passphraserecipient; postage = app.locals.formdata.postage; security = app.locals.formdata.security; minTrust = app.locals.formdata.minTrust; result1 = createWalletUsingActiveCard() ( ===result1) { doErrorMessage(res, ); } maxwait = ; { (currentPacketId=== ) { sleep( ); maxwait--; } { ; } } (maxwait> ); .log( , currentPacketId) result = fundAddress(api, currentPacketId, ); ( ===result) { doErrorMessage(res, ); } maxwait = ; { packetaccount = getAccount(api, currentPacketId); ( ===packetaccount) { sleep( ); maxwait--; } { ; } } (maxwait> ); ( ===result) { doErrorMessage(res, ); } { .log( , passphrasesender) senderAddress = getAddressForPassphrase(passphrasesender); registerPackageTransaction = RegisterPacketTransaction({ : { : transactions.utils.convertLSKToBeddows(security), : (minTrust), : transactions.utils.convertLSKToBeddows(postage), : currentPacketId, : getAddressForPassphrase(passphraserecipient), }, : transactions.utils.convertLSKToBeddows( ), : getNonceForAddress(api, senderAddress) }); .log( , registerPackageTransaction) registerPackageTransaction.sign(networkIdentifier,passphrasesender); registerresult = api.transactions.broadcast(registerPackageTransaction.toJSON()); doInfoMessage(res, ); } (ex) { doErrorMessage(res, + .stringify(ex.message, , )); } } (ex) { doErrorMessage(res, + .stringify(ex.message, , )); } } } const ( ) => app, api async // save current values if 'Fund Sender with 100 LSK' // fund button pressed let let await "100" if `Fund sender with 100 LSK success` ${participantAddress} else `Fund sender with 100 LSK failed` ${participantAddress} else if 'Fund carrier with 1500 LSK' // fund button pressed let let await "100" if `Fund carrier with 100 LSK success` ${participantAddress} else `Fund carrier with 100 LSK failed` ${participantAddress} else try // // step 1 -> make sure that the card has no wallet if false "This card already has an address, finish the transport before using it again" const const const const const let await if false "Unable to create wallet on this card" let 15 // max waittime in seconds do if false await 1000 else break while 0 console "got packetId %s" let await "100" // enough for 1000 measurements @ 0.1 lsk each if false `Funding failed for , finish the transport before using it again` ${currentPacketId} 30 // max waittime in seconds do let await if false await 1000 else break while 0 if false "Timeout while waiting for completion of fund transaction" try console "got senderpassphrase %s" let const new asset security minTrust Number postage packetId recipientId fee '0.1' nonce await console "got transaction %o" let await 'package registration complete' catch "Packet registration failed: " JSON null 2 catch "Packet registration failed: " JSON null 2 The process starts at line #40. A call to initializes a new wallet on the smart card. A wait loop on line #45 is required because it takes a while for the new wallet to be detected by the application: variable receives the wallet address. createWalletUsingActiveCard currentPacketID An interesting bit of code related to this can be seen in the handlers.js file in the client folder. checkReader = { { reader = getReader(); ( !==reader) { activeCardData = getActivecardData() haveCard = ; currentPacketPublicKey = ; currentPacketId = ; (activeCardData && activeCardData) { currentPacketPublicKey = activeCardData.WalletPublicKey.toString( ); currentPacketId = cryptography.getAddressFromPublicKey(activeCardData.WalletPublicKey) } } { haveCard = ; }; } (ex) { .error( , ex.message) } { setTimeout(checkReader, ); } } => () try let if false let true false false if "WalletPublicKey" in 'hex' else false // console.log("hc %s / cpid %s", haveCard, currentPacketId); catch console "app.checkReader - error " finally 1000 This function is called periodically in the background using setTimeout. In this function, the NFC card reader is polled (line #5) and the is retrieved. If a wallet is present on the card, the generic wallet public key is converted to a blockchain address using the function from the Lisk SDK cryptography library. checkReader activeCardData getAddressFromPublicKey The process in the function now continues at line #55 where some funds are sent to the card. These funds are required when submitting transactions that are signed by the wallet on the card to the blockchain. handlerRegisterSmartPackage Again, a wait loop is entered (line #61) that halts until the account associated with the smartcard wallet has been located on the blockchain. getAccount = (api, address) => { { accounts = api.accounts.get({address}) (accounts.data.length> ) { accounts.data[ ] } ; } (ex) { ; } } // client/functions.js #49 const async try let await if 0 return 0 else return false catch return false The function (from functions.js#49) implements this with a call to the Lisk SDK (line #5). getAccount Finally, at line #76 in the function, a registerPackageTransaction is instantiated and signed by the package sender (line #93). handlerRegisterSmartPackage After broadcasting this transaction to the blockchain network, package registration is complete and the function exists. Once the transaction is committed to the blockchain, a valid package account with initial funds now exists on the smartcard. Signing measurement data After starting the transport (not described here), the sensor starts collecting data and committing this data to the blockchain using a custom transaction on the sidechain. To prove the origin of the data, each package of measurement data is signed using the wallet on the smartcard. register-measurement The important part of the custom transaction is shown below: register-measurement applyAsset(store) { errors = []; packet = store.account.get( .senderId); (packet === ) { errors.push( TransactionError( , .id, , .asset.id, )); } (packet.status=== ||packet.status=== ) { timestamp = dateToLiskEpochTimestamp( ()); ( === packet) { packet.asset = {} }; ( === packet.asset) { packet.asset.measurements = [] }; packet.asset.transportstatus = ; packet.asset.measurements.push({ : .asset.timestamp, : .asset.temperature, : .asset.humidity }); store.account.set(packet.address, packet); } errors; } // transactions/register-measurement.js #27 const const this if undefined new "packet not found" this "this.asset.id" this "An existing packet ID on recipient account" if 'ongoing' 'alarm' let new Date if false "asset" in if false "measurements" in 'active' timestamp this temperature this humidity this return While the transport is ongoing (line #10) new (simulated) measurements are stored in the measurements array in the packet’s asset structure (line #15). The client application visualizes these measurements in a graph on the smart package page, (see above at Step 4: Track transport and add measurements). A demonstration of the usage of this transaction is found in the temperature_and_humidity app. This is code that runs inside of the sensor module and is called periodically to measure the transport conditions. In this case, to be able to demonstrate the principle without actual hardware, the measurements are simulated. This can be seen here below: doMeasurements = () => { goFast = ; { .log( ) reader = getReader(); (reader) { rescanActiveCard(); activeCardData = getActivecardData() ( ===activeCardData) { .log( ); ; } ( === activeCardData.WalletPublicKey) { .log( ); ; } packetPublicKey = activeCardData.WalletPublicKey.toString( ); packetID = cryptography.getAddressFromPublicKey(activeCardData.WalletPublicKey) packetAccount = getAccount(packetID); goFast = ; ( ===packetAccount) { .log( , packetID); ; } ( packetAccount.asset.status !== ){ .log( , packetID, packetAccount.asset.status); ; } { goFast = ; } gSensorState.temperature += (gSensorState.temperatureUp ? : ) * .random(); (gSensorState.temperature> ) { gSensorState.temperatureUp = } (gSensorState.temperature< ) { gSensorState.temperatureUp = } gSensorState.humidity += (gSensorState.humidityUp ? : ) * .random(); (gSensorState.humidity> ) { gSensorState.humidityUp = } (gSensorState.humidity< ) { gSensorState.humidityUp = } measurement = { : ().getTime() / , : .round(gSensorState.temperature* )/ , : .round(gSensorState.humidity* )/ }; tx = RegisterMeasurementTransaction({ networkIdentifier, : packetPublicKey, : measurement, : transactions.utils.convertLSKToBeddows( ), : packetAccount.nonce }); transactionWithNetworkIdentifierBytes = Buffer.concat([ cryptography.hexToBuffer(networkIdentifier), tx.getBytes() ]); datatosign = cryptography.hash(transactionWithNetworkIdentifierBytes) .log( , datatosign.length, datatosign.toString( )) tx.signatures = [ signDataUsingActiveCard(datatosign, )]; ( !==tx.signatures[ ]) { .log( ) tx._id = transactions.utils.getId(tx.getBytes()); { res = api.transactions.broadcast(tx.toJSON()); .log( ); .log(res.data); .log( ); .log(tx.stringify()); .log( ); } (ex) { .error( , ex.errno) } } { .error( ); } } { .log( ) } } (ex) { .error( , ex.message); } { setTimeout(doMeasurements, goFast? : ); } } // iot/temperature_and_humidity.js #30 const async let true try console "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" let if await let if false console "no card in reader" return if undefined console "not a valid smart package" return let 'hex' let let await true if false console "packet with Id %s has not yet been registered" return else if 'ongoing' console "packet with Id %s - transport not active (status: %s)" return else // console.log("packet data: %o", packetAccount.asset); false // simulate gradual changes in temperature and humidity 1 -1 Math if 30 false else if 5 true 1 -1 Math if 70 false else if 40 true let timestamp new Date 1000 temperature Math 100 100 humidity Math 100 100 // create measurement transaction let new senderPublicKey asset fee '0.01' nonce // console.log("got transaction %o", tx) // console.log(tx.getBytes().toString('hex')); // sign transaction using on-card wallet const // sign hash of data with the card const console "got data to sign (%s bytes) %s" 'hex' await false if false 0 console "sending transaction" // debug code: use verify function to check signature // console.log(tx.validate()); try let await console "++++++++++++++++ API Response +++++++++++++++++" console console "++++++++++++++++ Transaction Payload +++++++++++++++++" console console "++++++++++++++++ End Script +++++++++++++++++" catch console "broadcast transaction error %s" else console "signing with card failed" else console 'no active wallet' catch console "doMeasurements error - %s" finally 1000 15000 As already illustrated above, the active smartcard data is retrieved at line #21. Temperature data is generated at line #38 and stored in a measurement record at line #52. Next, a is instantiated. RegisterMeasurementTransaction Now things start working differently when compared to using a local wallet account. At line #71, the transaction is converted to a byte array and a prefix consisting of the network identifier is added. The hash of this array of bytes is calculated at line #77 (using the hash function from the cryptography library in the Lisk SDK). This hash is sent to the smartcard for signing using the function. The returned signature is added to the transaction (line #79). signDataUsingActiveCard Finally, an id for the transaction is calculated (line #85) and the transaction is broadcast to the blockchain network (line #87). Erasing the wallet on the smartcard Once the transport is complete, the wallet on the smartcard can be erased so that the card can be used for a new transport. This is done with a call to the function in the library as depicted below. This function provides a good illustration of how the communication with the smartcard works under the hood: purgeWallet tangemcard # tangem-smart/tangemcard.js exports.purgeWallet = ( reader, cid = , pin1 = , pin2 = , ) => { { pin1Hex = crypto .createHash( ) .update(Buffer.from(pin1)) .digest( ); pin2Hex = crypto .createHash( ) .update(Buffer.from(pin2)) .digest( ); tlv; tlv1 = Buffer.from( + cid, ); tlv2 = Buffer.from( + pin1Hex, ); tlv3 = Buffer.from( + pin2Hex, ); tlv = Buffer.concat([tlv1, tlv2, tlv3]); base = Buffer.from([ , , , , tlv.length, ]); request = Buffer.concat([base, tlv]); response = reader.transmit(request, ); sw1 = response[response.length - ] sw2 = response[response.length - ] sw = * sw1 + sw2; (sw=== ) { decodeTLV(response); } { .error( , sw.toString( )) ; } response; } (ex) { .error( , ex.message) ; } }; async 'BB03000000000004' '000000' '000' try const 'sha256' 'hex' const 'sha256' 'hex' let let '0108' 'hex' let '1020' 'hex' let '1120' 'hex' let 0x00 // Class 0xFC // INS: Purge wallet command 0x00 // P1: 0x00 // P2 // Le: Full Length of UID let let await 8192 // console.log("response %s", response.toString('hex')); let 2 let 1 let 256 if 0x9000 // console.log("purgeWallet OK (%s)", sw.toString(16)) return else console "purgeWallet ERROR (card response %s)" 16 return false return catch console "purgeWallet ERROR %s" return false At line #35, a request is transmitted to the smartcard reader. This request is a byte array that is composed by joining a number of data structures (line #24 and line #34) such as the card id (line #21), two hashed pin codes (line #22, 23), and command data (#26). After the command has been processed by the card, the returned data structure (response, line #35) is decoded to determine if the request was processed successfully. In this case, command code 0xFC (line #28) tells the smart card that a purge wallet command is requested. Lisk Builders This project was initiated by Caspar Roelofs who secured a Lisk Builders grant to enable the use of smartcard chips for private key management in Lisk blockchain solutions. As a founder of Gimly Blockchain Projects , he helps clients leverage blockchain, IoT, and other new technologies for their business needs. Gimly believes that meaningful innovation is achieved through synergies, and maximalizes collaboration to build on the strengths of clients, partners, and community. Consultancy and development services include end-to-end product development, solutions architecture, technology, product and market research, product management, and business development. Marc Buma joined the project as lead developer, with his expertise in IoT, blockchain, and JavaScript development. Marc started his career as an R&D engineer at Noldus Information Technology and worked on the development of automated behavior measurement systems, and has been self-employed, (Company name Bumos) since 2004. He has worked on broadcast innovation projects with Jetix, Disney, MTV, KPN, and the Netherlands Institute for Sound and Vision, and has a solid background in developing industrial strength systems. Currently, he is developing and maintaining bespoke products for hospitality onboard luxury yachts, blockchain based decentralized logistics tracking systems in the fashion industry, and VR guided tours. Marc is active as a mentor/technical advisor for several startups and projects in the blockchain and logistics sphere. He is also an avid panorama photographer with a passion for preserving cultural heritage and an author of a virtual travel guide. Working with Tangem by Gimly If you are interested to use the technology in your own projects, visit to find out how to obtain a trial kit, or enquire about if you are looking for higher volumes of customized cards, stickers, or tags with custom design and wallet for your cryptocurrency. gimly.io business to business options Disclaimer: This blog post was written by our community members, Caspar Roelofs ( LinkedIn profile ) and Marc Buma ( LinkedIn profile ) as part of their participation in the Lisk Builders program.