Introduction
Infineon’s Blockchain Security 2Go starter kit (BLOCKCHAINSTARTKITTOBO1-ND) physically contains nothing more than five credit card sized NFC smart cards (and an elaborately folded leaflet). The embedded Infineon security controllers and software in these cards along with the open-source auxiliary software, available from Infineon’s GitHub repository, come together to form a kit which allows users to integrate hardware-based security into their blockchain applications with relative ease. With the cards, users can generate and store up to 255 private/public key pairs, import an existing key pair, and digitally sign 32-byte hashes with the Elliptic Curve Digital Signature Algorithm (ECDSA). There is also the option for PIN authentication. Any blockchain that utilizes the secp256k1 elliptic curve for its cryptographic operations, as do Bitcoin and Ethereum, is compatible with these cards [1].
To get started, developers can utilize the example Android application, the example PC application (Windows, Linux, or Mac), and/or the BlockSec2Go Python library. The Android application is, of course, written in Java and demonstrates how to interact with Ethereum. The PC application is written in Python and demonstrates how to interact with Bitcoin. This tutorial is for those wishing to develop an application in Python leveraging the Security 2Go cards to interact with Ethereum.
Background
I will confess right off the bat that I am by no means a blockchain expert. Therefore, I will direct readers who are unfamiliar with the core concepts of blockchain; such as cryptographically secured blocks of transactions, consensus algorithms, etc.; to either of Andreas Antonopoulos’s books [2] or [3] as they are comprehensive and freely available on GitHub. These books also cover some cryptography basics that the reader should be familiar with, such as hashing, private/public key pairs, and digital signatures.
Asymmetric cryptography plays a vital role in blockchain applications. As distributed systems where all transactions are unencrypted and available for anyone to examine, blockchains rely solely on private keys to control account assets like currency or smart contracts. Losing one’s private keys equates to total loss of control over these assets. What’s more, because blockchains are immutable, any transaction created by a bad actor using stolen private keys can never be revoked. The importance of protecting one’s private keys cannot be understated and, as explained in detail by [4], there are many ways that traditional key management approaches can be compromised. Infineon’s hardware-based key management solution not only resolves these issues but does so in a manner that is easy for developers to implement.
The Security 2Go cards feature a simple API with a limited command set, shown in Table 1. There is one session command , called SELECT APP, which must be the first command issued to the card after it is recognized by the reader. Otherwise, none of the other commands will work. There are four basic commands , which provide key management functionality. The GET KEY INFO and GENERATE SIGNATURE commands will be used repeatedly throughout this tutorial. The PIN commands, which provide authentication capability, will not be utilized in this tutorial to avoid confusion. Note that there are no available commands to perform other cryptographic operations such as hashing, encryption/decryption, signature verification, or key agreement. These operations either don’t require the private key or are not related to blockchain security. Therefore, they must be implemented by the main application (i.e., off-card) when needed [1].
These cards may be purchased in increments of either five cards (BLOCKCHAINSTARTKITTOBO1-ND) or ten cards (BLOCKCHAIN10CARDSTOBO1-ND). Naturally, to make use of them you will need a contactless NFC reader. More specifically, you will need a reader compliant with the ISO/IEC 14443A communication standard. The code in the following sections (available from GitHub) has been tested with the Omnikey 5422 dual interface reader (Figure 1).
Figure 1: HID OMNIKEY 5422 dual interface contact and contactless smart card reader [5]
Creating a Private Ethereum Blockchain
To test the python code provided in the below sections, a private Ethereum blockchain was created and run on the same PC. Two geth instances are launched and connected, effectively creating two Ethereum nodes that communicate with each other. Once you develop your application and get it working on a private blockchain, you can move onto a testnet (e.g., Rinkeby) to perform final testing before graduating to mainnet. Because we are the only users of our private blockchain, there is little point in constantly mining new, empty blocks with the proof-of-work (PoW) algorithm. Therefore, the Clique consensus engine was chosen in the procedure below to implement proof-of-authority (PoA) mining and the sealing period was set to zero. This way, blocks will only be created and mined as transactions become pending.
There are many approaches one could take to set up a private Ethereum blockchain and the procedure provided below is just one of them. Because the details of this setup are out of the scope of this tutorial, the explanations have been kept as concise as possible.
Step 1: Install Geth
Start by installing the latest version of Geth and the development tools, available from the download page. Once done, be sure the ‘geth’ and ‘puppeth’ commands are available for use. Geth version 1.9.8 was used for this tutorial.
~$ geth version | head -n 8
Geth
Version: 1.9.8-stable
Git Commit: d62e9b285777c036c108b89fac0c78f7855ba314
Git Commit Date: 20191126
Architecture: amd64
Protocol Versions: [64 63]
Go Version: go1.13.4
Operating System: linux
Step 2: Create the Accounts
Create a directory to hold the private chain data and enter that directory.
~$ mkdir mychain
~$ cd mychain/
There will be two nodes running and each node will have one account. The following commands will create a subdirectory for each node containing the keys for the new accounts. Each account requires a password, so you will be prompted to provide one. Once you do, the account address will be displayed. Note the two addresses for the next step.
~/mychain$ geth --datadir ./node1 account new
~/mychain$ geth --datadir ./node2 account new
At this point, the account passwords can be saved to files as shown below. This will make it easier to start the nodes in Step 5. Replace “pwdnode1” and “pwdnode2” with the passwords you chose.
~/mychain$ echo "pwdnode1" >> ./node1/password.txt
~/mychain$ echo "pwdnode2" >> ./node2/password.txt
Step 3: Create the Genesis Block
To create the genesis block, we will use the puppeth tool. Simply execute the ‘puppeth’ command and follow the prompts as shown below. Note that you should provide your own account addresses (from Step 2) to the questions “Which accounts are allowed to seal?” and “Which accounts should be pre-funded?” Where the prompt is blank, no input was provided to accept the default option.
~/mychain$ puppeth
+-----------------------------------------------------------+
| Welcome to puppeth, your Ethereum private network manager |
| |
| This tool lets you create a new Ethereum network down to |
| the genesis block, bootnodes, miners and ethstats servers |
| without the hassle that it would normally entail. |
| |
| Puppeth uses SSH to dial in to remote servers, and builds |
| its network components out of Docker containers using the |
| docker-compose toolset. |
+-----------------------------------------------------------+
Please specify a network name to administer (no spaces, hyphens or capital letters please)
> mynet
Sweet, you can set this via --network=mynet next time!
INFO [01-17|16:39:11.782] Administering Ethereum network name=mynet
WARN [01-17|16:39:11.782] No previous configurations found path=/home/matt/.puppeth/mynet
What would you like to do? (default = stats)
1. Show network stats
2. Configure new genesis
3. Track new remote server
4. Deploy network components
> 2
What would you like to do? (default = create)
1. Create new genesis from scratch
2. Import already existing genesis
> 1
Which consensus engine to use? (default = clique)
1. Ethash - proof-of-work
2. Clique - proof-of-authority
> 2
How many seconds should blocks take? (default = 15)
> 0
Which accounts are allowed to seal? (mandatory at least one)
> 0x7242F20C58E0936da85850eef4E49907B49e40dc
> 0x9C2EA7815000a872Df56a4dBF127F60aBddFb9e2
> 0x
Which accounts should be pre-funded? (advisable at least one)
> 0x7242F20C58E0936da85850eef4E49907B49e40dc
> 0x9C2EA7815000a872Df56a4dBF127F60aBddFb9e2
> 0x
Should the precompile-addresses (0x1 .. 0xff) be pre-funded with 1 wei? (advisable yes)
>
Specify your chain/network ID if you want an explicit one (default = random)
>
INFO [01-17|16:41:08.802] Configured new genesis block
What would you like to do? (default = stats)
1. Show network stats
2. Manage existing genesis
3. Track new remote server
4. Deploy network components
> 2
1. Modify existing configurations
2. Export genesis configurations
3. Remove genesis configuration
> 2
Which folder to save the genesis specs into? (default = current)
Will create mynet.json, mynet-aleth.json, mynet-harmony.json, mynet-parity.json
>
INFO [01-17|16:42:27.597] Saved native genesis chain spec path=mynet.json
ERROR[01-17|16:42:27.597] Failed to create Aleth chain spec err="unsupported consensus engine"
ERROR[01-17|16:42:27.597] Failed to create Parity chain spec err="unsupported consensus engine"
INFO [01-17|16:42:27.598] Saved genesis chain spec client=harmony path=mynet-harmony.json
What would you like to do? (default = stats)
1. Show network stats
2. Manage existing genesis
3. Track new remote server
4. Deploy network components
> ^C
Delete the *-harmony.json file and rename the other file to “genesis.json”.
~/mychain$ rm mynet-harmony.json
~/mychain$ mv mynet.json genesis.json
~/mychain$ ls -l
total 32K
-rw-r--r-- 1 matt matt 22K Jan 17 16:42 genesis.json
drwx------ 3 matt matt 4.0K Jan 17 16:30 node1
drwx------ 3 matt matt 4.0K Jan 17 16:30 node2
Step 4: Initialize the Nodes
Use the following commands to initialize the nodes with the genesis block.
~/mychain$ geth --datadir ./node1 init genesis.json
~/mychain$ geth --datadir ./node2 init genesis.json
Step 5: Start the Nodes
First, find and note the Chain ID from the genesis.json file.
~/mychain$ head genesis.json
{
"config": {
"chainId": 42586,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
Open two terminals. In the first terminal, execute the following command, replacing the networkid value with the Chain ID of your private chain and the address of the account to unlock with your account address. Because the ‘console’ command was given, a prompt should appear in the terminal once the node has started.
~/mychain$ geth --datadir ./node1 --syncmode full --networkid 42586 --port 30311 --ipcdisable --rpc --rpcaddr 'localhost' --rpcport 8501 --rpcapi 'personal,db,eth,net,web3,txpool,miner,admin' --allow-insecure-unlock --unlock '0x7242F20C58E0936da85850eef4E49907B49e40dc' --password ./node1/password.txt –mine console
In the second terminal, execute the following command, again replacing the networkid value and account address with your own. The ‘console’ command was not given this time, so no prompt should appear after the node starts.
~/mychain$ geth --datadir ./node2 --syncmode full --networkid 42586 --port 30312 --ipcdisable --rpc --rpcaddr 'localhost' --rpcport 8502 --rpcapi 'personal,db,eth,net,web3,txpool,miner,admin' --allow-insecure-unlock --unlock '0x9C2EA7815000a872Df56a4dBF127F60aBddFb9e2' --password ./node2/password.txt --mine
Step 6: Connect the Nodes
Search the start-up log of Node 2 (the second terminal) for the enode URL and copy it (see the highlighted portion in Figure 2).
In the first terminal, enter the following command at the prompt, pasting your enode URL as the function argument (don’t forget the quotation marks). It should return true.
> admin.addPeer( "enode://86124e42d7b4a03edfa0185e83c0a661faead4cbfb0544a5fac20ff11df0ef174e086a5c76e2f3cdc7ae67be2f193fc166cfe5b8a8c0d1b31ef0c836b03db66c@127.0.0.1:30312" )
true
Both nodes should now be running and communicating with each other. To stop them, simply type ‘exit’ in the first terminal and ‘Ctrl+c’ in the second terminal. Be sure to start the nodes again (Steps 5-6) before executing any of the examples provided below.
Prepping Python
The following examples of using the Security 2Go cards to interact with the Ethereum blockchain are written in Python. To follow along and run the examples, you will have to install the following python modules: web3 and blocksec2go . Note that blocksec2go requires swig be installed (see Infineon’s Blockchain Security 2Go Library repository for details).
To test your python environment, run the code provided in Listing 1. It will connect to Node 1 and list the available card readers.
Listing 1: environment_test.py (available on GitHub)
import blocksec2go
from web3 import Web3, HTTPProvider
# connect to the Ethereum node
w3 = Web3( HTTPProvider( "http://127.0.0.1:8501" ) )
# check for successful connection
if not w3.isConnected():
print( "Unable to connect to Ethereum node" )
raise SystemExit()
# get list of available readers
print( blocksec2go.readers() )
Creating a New EOA With the Card
An externally owned account (EOA) is “an account created by or for human users of the Ethereum network” [3]. Unlike contract accounts, which are defined by smart contract code, EOAs are fundamentally defined by a private key. By generating a new private/public key pair on the Security 2Go card, we are essentially creating a new EOA. The account address is derived from the public key and the account transaction history, balance, and nonce (transaction count) are recorded on the blockchain. Note that the account nonce is different than the PoW nonce used in mining.
Infineon already has an excellent example showing how to use the BlockSec2Go python library to generate/store a new key pair and obtain the resulting public key. The example includes two useful functions. The get_reader()
function will find the desired card reader with a smart card connected to it and the activate_card()
function will send the SELECT APP command to activate the other commands on the card. These functions will be used in all the subsequent examples in this article. Before moving on, be sure to have at least one key pair generated on the card.
The public key of a specific key slot obtained from the card will be Sec1 encoded and uncompressed [6]. All this means for us is that the returned key will have a one-byte prefix of 0x04
. We need to remove this byte before we use the key to derive the account address.
global_counter, counter, public_key_sec1 = blocksec2go.get_key_info( reader, key_id )
public_key = public_key_sec1[1:]
Next, the Keccak-256 hash of the public key is calculated using the Web3 base API (see the Web3.py documentation for more details).
public_key_hash = w3.keccak( public_key )
The last 20 bytes of the hash are the account address. The address can optionally be converted to a hexadecimal string to make it easier to work with.
inf_card_addr = public_key_hash[-20:].hex()
Listing 2 shows how the address of the account defined by the private key in key slot 1 is derived and used to receive ether from an Ethereum node.
Listing 2: Selected code from receive_ether.py (available on GitHub)
# print address and balance of first account on ethereum node
print( f'Address of account 0 on full node: { w3.eth.accounts[0] }' )
print( f'Balance of account 0 on full node: { w3.fromWei( w3.eth.getBalance( w3.eth.accounts[0] ), "ether" ) }' )
print()
# derive and print address of an account on infineon card. Print account balance.
key_id = 1
reader = get_reader()
activate_card( reader )
public_key = get_public_key( reader, key_id )
inf_card_addr = w3.toChecksumAddress( w3.keccak( public_key[1:] )[-20:].hex() )
print( f'Address of account {key_id} on Infineon card: { inf_card_addr }' )
print( f'Balance of account {key_id} on Infineon card: { w3.fromWei( w3.eth.getBalance( inf_card_addr ), "ether" ) }' )
print()
# NOTE: 'gasPrice', 'gas', and 'chainId' are filled in automatically
# NOTE: 'from' is included so the sendTransaction function knows which private/public key pair to use. It will not be included on the block chain explicitly as it can be recovered from the signature.
tx_hash = w3.eth.sendTransaction( { 'from': w3.eth.accounts[0], 'to': inf_card_addr, 'value': w3.toWei( 10, 'ether' ) } )
print( "Sending 10 ether from full node account to Infineon card account..." )
# wait for transaction to be included on a block
tx_receipt = w3.eth.waitForTransactionReceipt( tx_hash )
print()
# print account balances
print( f'Balance of account 0 on full node: { w3.fromWei( w3.eth.getBalance( w3.eth.accounts[0] ), "ether" ) }' )
print( f'Balance of account {key_id} on Infineon card: { w3.fromWei( w3.eth.getBalance( inf_card_addr ), "ether" ) }' )
The output of Listing 2 should be similar to the following.
~/blocksec2go_ethereum_examples$ python .\receive_ether.py
Address of account 0 on full node: 0x7242F20C58E0936da85850eef4E49907B49e40dc
Balance of account 0 on full node: 904625697166532776746648320380374280103671755200316906558.262375061821325312
Found the specified reader and a Blockchain Security 2Go card!
Address of account 1 on Infineon card: 0xd612f147159200856f32Fbcfc3201bb75C511774
Balance of account 1 on Infineon card: 0
Sending 10 ether from full node account to Infineon card account...
Balance of account 0 on full node: 904625697166532776746648320380374280103671755200316906548.262375061821325312
Balance of account 1 on Infineon card: 10
Spending the Card’s Ether
Receiving ether is the easy part. Spending ether, however, requires the user to be in possession of the account’s private key to generate a signature for the transaction. While we cannot extract the private key from the Security 2Go card, we can use it to sign our transaction with the GENERATE SIGNATURE command. The procedure for doing so is shown below in Figure 3. Unfortunately, the web3 API does not provide any high-level method of hashing transactions or working with signatures. The easiest way I have found to do this is by utilizing two utility functions in the eth-account module (which was installed with the web3 module as a dependency).
from eth_account._utils.transactions import serializable_unsigned_transaction_from_dict, encode_transaction
Step 0: Derive the Account Address from the Public Key
This prerequisite step was outlined in the previous section. The account address will be needed to determine the signature prefix in Step 4.
Step 1: Create and Encode the Transaction
In the previous example we created a partial transaction, specifying only the ‘from’, ‘to’, and ‘value’ fields. The Eth.sendTransaction()
method filled in the remaining fields (‘nonce’, ‘gasPrice’, ‘gas’, ‘data’, ‘v’, ‘r’, and ‘s’) for us. Using the procedure diagramed in Figure 3, a dictionary specifying ‘to’, ‘value’, ‘gas’, ‘gasPrice’, ‘nonce’, and optionally ‘chainId’ will have to be created manually. The ‘v’, ‘r’, and ‘s’ fields constitute the signature and will be derived in Steps 3 and 4. Note that the ‘from’ field does not have to be specified as it will be derived from the signature.
nonce = w3.eth.getTransactionCount( inf_card_addr )
transaction = { 'to': w3.eth.accounts[0], 'value': w3.toWei( 1, "ether" ), 'gasPrice': w3.eth.gasPrice, 'nonce': nonce, 'chainId': w3.eth.chainId }
transaction['gas'] = w3.eth.estimateGas( transaction )
Let’s take a closer look at each of these fields.
to: The address of the account which will be receiving the ether we are sending. In this case, we are sending ether to the first account of the node to which we are connected (Node 1).
value: The amount of ether we are sending. In this case, we are sending 1 ether (or 1x1018 wei).
gasPrice: The price of gas obtained via the Eth.gasPrice
property.
nonce: This is simply a tally of the number of transactions sent from the originating address, used to prevent replay attacks. We can easily get the number of confirmed account transactions on the blockchain with the Eth.getTransactionCount()
method. However, if you are creating several transactions from an account with short delays between them, you could end up with several pending transactions that will not be counted by Eth.getTransactionCount()
. In this case, transactions will have to manually be counted by the application.
chainId: The chain identifier was specified when the nodes were started (as networkid). We can access it via the Eth.chainId
property. This field is optional and was introduced in EIP-155 (EIP stands for Ethereum Improvement Proposal) as a means to further prevent replay attacks. We will come to this again in Step 4.
gas: We add this field last using the Eth.estimateGas()
method. This method takes the transaction dictionary as an argument and returns an estimate of the gas that will be consumed.
Once we have the transaction structure defined, it must be converted to a byte stream, i.e. serialized . Ethereum utilizes the Recursive Length Prefix (RLP) encoding scheme to accomplish this. Using the serializable_unsigned_transaction_from_dict()
function we imported earlier, the dictionary can easily be encoded as a serializable object.
unsigned_encoded_transaction = serializable_unsigned_transaction_from_dict( transaction )
Step 2: Calculate the Transaction Hash
The benefit of creating a serializable object is the included hash()
method. We can easily calculate the transaction hash as shown below in the python interpreter.
>>> unsigned_encoded_transaction.hash()
HexBytes('0x3ceba808421a63d494ab259a747389bf11b78a0ca639a13f02b654cae5a58031')
The hash may also be obtained manually by calculating the Keccak-256 hash of the RLP-encoded transaction. This is the same algorithm used to hash the public key when deriving the account address.
>>> import rlp
>>> w3.keccak( rlp.encode( unsigned_encoded_transaction ) )
HexBytes('0x3ceba808421a63d494ab259a747389bf11b78a0ca639a13f02b654cae5a58031')
Step 3: Use the Card to Generate the Signature
Just like with generating key pairs and obtaining the public keys, Infineon has an excellent example of generating a signature given a 32-byte hash value. The returned signature is ANS.1 DER encoded [7] as shown in Table 2.
There are many ways to extract the r and s components of the signature. You could use the ASN1 module or you could write your own function. An example of such a function is provided in Listing 3.
Listing 3: Function to extract signature components from ANS1.DER encoding
def get_signature_components( der_encoded_signature ):
# check signature lengthl
if len( der_encoded_signature ) < 2:
print( "Invalid signature!" )
raise SystemExit
# if does not start with signature DER TAG
if not der_encoded_signature.startswith( b'\x30' ):
print( "Invalid signature!" )
raise SystemExit
# get signature length
sig_len = der_encoded_signature[1]
if sig_len != len( der_encoded_signature[2:] ):
print( "Signature length incorrect" )
raise SystemExit
pos = 2
components = []
while sig_len > 0:
# if does not start with component DER TAG
if der_encoded_signature[pos] != 0x02:
print( "Expecting component DER TAG" )
raise SystemExit
pos += 1
# get the component length
component_len = der_encoded_signature[pos]
pos += 1
# get the component
components.append( int.from_bytes( der_encoded_signature[pos:pos+component_len], byteorder='big' ) )
pos += component_len
sig_len = sig_len - component_len - 2
return components
Step 4: Determine the Signature Prefix
Given the transaction signature obtained in the previous step (i.e. r and s ) and the transaction hash, the signer’s public key can be calculated. However, like how sqrt(4)
could be either 2 or -2, there will be two possible public keys that result from this calculation. There is no way to tell which one is the signer’s public key, and which one isn’t. Therefore, a third component needs to be included with the signature to resolve this ambiguity. It’s called the signature prefix and it is denoted by the letter v .
Following Ethereum’s original signature scheme, v can be either 27 or 28. With the new signature scheme defined in EIP-155, v can be either CHAIN_ID * 2 + 35
or CHAIN_ID * 2 + 36
. Both are still valid, but the old scheme cannot be used if ‘chainId’ was specified in the transaction dictionary back in Step 1. The way we determine v is simply by guessing and checking. Choose one value for v and calculate the address of the account which must have originated the transaction (recall that the address is derived from the public key). If the address returned did in fact originate the transaction, v was chosen correctly. If the address is incorrect, simply choose the other value of v and perform the calculation again. If the address is still incorrect after this second calculation, then the signature is not valid.
The _recover_hash()
method is the simplest way I have found to recover the originating address. The function in Listing 4 uses it to calculate the address given each value of v and compare it with the expected address. Assuming the signature is valid, the correct value of v is returned.
Listing 4: Function to obtain signature prefix
def get_signature_prefix( signature_rs, address, transaction_hash, chainId=-4 ):
try:
r, s = signature_rs
except:
print( "Invalid signature argument!" )
raise SystemExit()
v = chainId * 2 + 35
if w3.eth.account._recover_hash( bytes( transaction_hash ), vrs=( v, r, s ) ) != address:
v = chainId * 2 + 36
if w3.eth.account._recover_hash( bytes( transaction_hash ), vrs=( v, r, s ) ) != address:
print( "Could not verify the signature" )
raise SystemExit()
return v
Step 5: Encode the Signed Transaction
The final step in signing the transaction is appending the signature. The encode_transaction()
function we imported earlier takes the unsigned transaction and the signature as arguments and outputs the RLP-encoded signed transaction.
signed_encoded_transaction = encode_transaction( unsigned_encoded_transaction, vrs=( v, r, s ) )
Step 6: Send the Transaction
Finally, to send the signed, RLP-encoded transaction to the blockchain, use the Eth.sendRawTransaction()
method. On success, this method will return the transaction hash. Note that this is not the same transaction hash we calculated earlier. In Step 2 we calculated the hash of the unsigned transaction whereas this method returns the hash of the signed transaction.
tx_hash = w3.eth.sendRawTransaction( signed_encoded_transaction )
Listing 5 shows how ether belonging to an account defined by one of the private/public key pairs on the card can be sent to another account.
Listing 5: Selected code from send_ether.py (available on GitHub)
# create the transaction as a dictionary
nonce = w3.eth.getTransactionCount( inf_card_addr )
transaction = { 'to': w3.eth.accounts[0], 'value': w3.toWei( 1, "ether" ), 'gasPrice': w3.eth.gasPrice, 'nonce': nonce, 'chainId': w3.eth.chainId }
transaction['gas'] = w3.eth.estimateGas( transaction )
# serialize the transaction with the RLP encoding scheme
unsigned_encoded_transaction = serializable_unsigned_transaction_from_dict( transaction )
# sign the hash of the serialized transaction
global_counter, counter, signature = blocksec2go.generate_signature( reader, key_id, bytes( unsigned_encoded_transaction.hash() ) )
print( "Remaining signatures with card:", global_counter )
print( f"Remaining signatures with key {key_id}:", counter )
print( "Signature (hex):" + signature.hex() )
print()
# Extract r and s from the signature
try:
r, s = get_signature_components( signature )
except:
print( "Invalid signature components!" )
raise SystemExit()
# determine the signature prefix value
v = get_signature_prefix( ( r, s ), inf_card_addr, bytes( unsigned_encoded_transaction.hash() ), w3.eth.chainId )
# add the signature to the encoded transaction
signed_encoded_transaction = encode_transaction( unsigned_encoded_transaction, vrs=( v, r, s ) )
# send the signed, serialized transaction
tx_hash = w3.eth.sendRawTransaction( signed_encoded_transaction )
print( "Sending 1 ether from Infineon card account to full node account..." )
# wait for transaction to be included on a block
tx_receipt = w3.eth.waitForTransactionReceipt( tx_hash )
print()
# print account balances
print( f'Balance of account 0 on full node: { w3.fromWei( w3.eth.getBalance( w3.eth.accounts[0] ), "ether" ) }' )
print( f'Balance of account {key_id} on Infineon card: { w3.fromWei( w3.eth.getBalance( inf_card_addr ), "ether" ) }' )
Listing 5’s output is very similar to that of Listing 2. However, this time the transaction signature returned by the card is displayed along with the remaining available signatures.
~/blocksec2go_ethereum_examples$ python .\send_ether.py
Address of account 0 on full node: 0x7242F20C58E0936da85850eef4E49907B49e40dc
Balance of account 0 on full node: 904625697166532776746648320380374280103671755200316906548.262354061821325312
Found the specified reader and a Blockchain Security 2Go card!
Address of account 1 on Infineon card: 0xd612f147159200856f32Fbcfc3201bb75C511774
Balance of account 1 on Infineon card: 10
Remaining signatures with card: 999999
Remaining signatures with key 1: 99999
Signature (hex):3044022033677a8c8a1ba58671834fea7320648d0fc95da480a16b64908293f8cef037dd02203e35a721218e06747b1a5841b5ca661eb523921849e519d210329dc90f69d23b
Sending 1 ether from Infineon card account to full node account...
Balance of account 0 on full node: 904625697166532776746648320380374280103671755200316906549.262375061821325312
Balance of account 1 on Infineon card: 8.999979
Deploying a Smart Contract
While the previous two sections covered payment transactions, these next two will cover transactions which invoke smart contract functions. To this end, a simple contract named HelloWorld has been created based on the examples in [8] and [3] to demonstrate how certain functions can be “owned” such that only the holder of the Infineon card can execute them. The solidity code for this contract is shown in Listing 6. Though solidity will not be covered here in any depth, it should be fairly clear what this contract accomplishes. When the contract is first deployed, the constructor is called and the address of the EOA that created the contract is stored in the owner
variable. With the contract deployed on the blockchain, there are two functions available for invocation. The setMessage()
function stores the string parameter in the message
variable and the getMessage()
function reads and returns the contents of the message
variable. Notice, however, that the setMessage()
function first checks that the address invoking the function is the same address which originally created the contract. If the contract creation transaction was signed by one of the accounts on the Infineon card, then only that account can successfully call the setMessage()
function. However, this check is not present in the getMessage()
function and, therefore, anyone on the blockchain can execute it.
Listing 6: helloworld.sol (available on GitHub)
pragma solidity ^0.5.11;
contract HelloWorld
{
address payable owner;
string message = "hello world";
function setMessage( string memory msg_ ) public
{
require( msg.sender == owner );
message = msg_;
}
function getMessage() public view returns ( string memory )
{
return message;
}
constructor() public
{
owner = msg.sender;
}
}
To simplify things as much as possible, the Remix IDE was used to compile the solidity code. Figure 4 shows that once the code successfully compiles, the user can copy the Application Binary Interface (ABI) and Bytecode using the buttons circled in red. I’ve pasted these into two separate files (available in GitHub). Note that after pasting the bytecode, I deleted everything except the ‘object’ value.
Once the ABI and bytecode are saved to files, we can easily open the files in Python and load the data. Note that the json module is required to load the ABI. This compilation data is then used to create a contract object using Eth.contract()
method. Because the contract is not yet deployed, we do not have a contract address to supply to the contract object. To deploy the contract and obtain the address, we will use the Contract.constructor.buildTransaction()
method to create the transaction dictionary for us. However, we must specify the ‘nonce’ ourselves because it will not be included in the returned dictionary otherwise.
# load the abi and bytecode from the remix compiler
with open( 'solidity/helloworld_abi.json' ) as abi:
contract_abi = json.load( abi )
with open( 'solidity/helloworld_bytecode.txt' ) as bytecode:
contract_bytecode = bytecode.read()
# create contract object for deployment
contract = w3.eth.contract( abi=contract_abi, bytecode=contract_bytecode )
# create the transaction dictionary for contract creation
transaction = contract.constructor().buildTransaction( { 'nonce': w3.eth.getTransactionCount( inf_card_addr ) } )
Once the transaction dictionary is constructed, simply follow the steps outlined in the previous section to sign the transaction with the Security 2Go card and send it to the blockchain. In this case, however, it is very important to get the transaction receipt because it will contain the address of the contract we just deployed. Finally, we can initialize the contract object with the address and begin calling contract functions.
# wait for transaction to be included on a block
tx_receipt = w3.eth.waitForTransactionReceipt( tx_hash )
# add address to contract object
contract = contract( address=tx_receipt['contractAddress'] )
The Contract.functions
property exposes the contract functions to us. If the function does not change the state of the blockchain, we should execute it with the ContractFunction.call()
method. This will invoke the function locally and not create a public transaction on the blockchain. As such, no ether will be consumed. This is the case with the getMessage()
function in our example contract, as it simply reads the message
variable and doesn’t write to it.
# execute the getMessage() contract function and print the result
message = contract.functions.getMessage().call()
The example in Listing 7 deploys the HelloWorld contract to our private blockchain and then calls the getMessage()
function to obtain the default message.
Listing 7: Selected code from deploy_contract.py (available on GitHub)
# load the abi and bytecode from the remix compiler
with open( 'solidity/helloworld_abi.json' ) as abi:
contract_abi = json.load( abi )
with open( 'solidity/helloworld_bytecode.txt' ) as bytecode:
contract_bytecode = bytecode.read()
# create contract object for deployment
contract = w3.eth.contract( abi=contract_abi, bytecode=contract_bytecode )
# create the transaction dictionary for contract creation
transaction = contract.constructor().buildTransaction( { 'nonce': w3.eth.getTransactionCount( inf_card_addr ) } )
# serialize the transaction with the RLP encoding scheme
unsigned_encoded_transaction = serializable_unsigned_transaction_from_dict( transaction )
# sign the hash of the serialized transaction
global_counter, counter, signature = blocksec2go.generate_signature( reader, key_id, bytes( unsigned_encoded_transaction.hash() ) )
print( "Remaining signatures with card:", global_counter )
print( f"Remaining signatures with key {key_id}:", counter )
print( "Signature (hex):" + signature.hex() )
print()
# Extract r and s from the signature
try:
r, s = get_signature_components( signature )
except:
print( "Invalid signature components!" )
raise SystemExit()
# determine the signature prefix value
v = get_signature_prefix( ( r, s ), inf_card_addr, bytes( unsigned_encoded_transaction.hash() ), w3.eth.chainId )
# add the signature to the encoded transaction
signed_encoded_transaction = encode_transaction( unsigned_encoded_transaction, vrs=( v, r, s ) )
# send the signed, serialized transaction
tx_hash = w3.eth.sendRawTransaction( signed_encoded_transaction )
print( "Deploying smart contract..." )
# wait for transaction to be included on a block
tx_receipt = w3.eth.waitForTransactionReceipt( tx_hash )
print()
# print account balance
print( f'Balance of account {key_id} on Infineon card: { w3.fromWei( w3.eth.getBalance( inf_card_addr ), "ether" ) }' )
# get and print the contract address
contract_address = tx_receipt['contractAddress']
print( "Contract Address:", contract_address )
# add address to contract object
contract = contract( address=contract_address )
# execute the getMessage() contract function and print the result
message = contract.functions.getMessage().call()
print( "getMessage() returned:", f"\"{message}\"" )
The output of Listing 7 should resemble the following. Be sure to save the contract address at this point if you plan to use it in other applications.
~/blocksec2go_ethereum_examples$ python .\deploy_contract.py
Found the specified reader and a Blockchain Security 2Go card!
Address of account 1 on Infineon card: 0xd612f147159200856f32Fbcfc3201bb75C511774
Balance of account 1 on Infineon card: 8.999979
Remaining signatures with card: 999998
Remaining signatures with key 1: 99998
Signature (hex):304402207b6a676cd72c4bd4d63e7730abfdb9281f84902e2baeab33ec86b455e9fbb06d022043674d4e5551de11d7a8f88ee307ba7f2b58ef6a8362fb8562d2509ad9bce63c
Deploying smart contract...
Balance of account 1 on Infineon card: 8.999690856
Contract Address: 0xf7Bb1E956dC06C1804ec4788D5297BeB471a4397
getMessage() returned: "hello world"
Changing the State of the Smart Contract
While the getMessage()
function does not change the contract state, the setMessage()
function does by writing a new value to the message
variable. Therefore, a new transaction must be created on the blockchain either automatically via the ContractFunction.transact()
method or manually via the ContractFunction.buildTransaction()
method. Because the only way to execute this function is by invoking it with the Infineon card’s account (since it’s the account we used to deploy the contract), we will have to go the manual route.
This time, because the contract is already deployed on the blockchain and we know its address, the contract object is created by supplying the contract address and the ABI rather than the bytecode and the ABI. Then, the transaction dictionary is built using the ContractFunction.buildTransaction()
method. Just like in the previous section, the ‘nonce’ must be supplied manually. Unlike the previous section, however, the ‘from’ field must be specified as well. This is because the transaction will be executed locally in a dry-run to estimate the gas usage. If ‘from’ is not specified as the Infineon card’s account address, the execution will fail. Once the dictionary is returned, however, the ‘from’ field must be removed because the serializable_unsigned_transaction_from_dict()
function will not recognize it and fail.
# create the contract object for invocation
contract = w3.eth.contract( address=contract_address, abi=contract_abi )
# create the transaction dictionary for calling setMessage contract function
transaction = contract.functions.setMessage( "New Message" ).buildTransaction( { 'nonce': w3.eth.getTransactionCount( inf_card_addr ), 'from': inf_card_addr } )
del transaction['from']
Hereafter, everything is the same as the previous section. The example in Listing 8 will use the setMessage()
function to update the message with a string supplied by the user as a command line argument. It will then call the getMessage()
function so the user can verify that the update was successful. Before running this code yourself, be sure to replace my contract address with your own.
Listing 8: Selected code from invoke_contract.py (available on GitHub)
# create the contract object for invocation
contract = w3.eth.contract( address=contract_address, abi=contract_abi )
# create the transaction dictionary for calling setMessage contract function
transaction = contract.functions.setMessage( sys.argv[1] ).buildTransaction( { 'nonce': w3.eth.getTransactionCount( inf_card_addr ), 'from': inf_card_addr } )
del transaction['from']
# serialize the transaction with the RLP encoding scheme
unsigned_encoded_transaction = serializable_unsigned_transaction_from_dict( transaction )
# sign the hash of the serialized transaction
global_counter, counter, signature = blocksec2go.generate_signature( reader, key_id, bytes( unsigned_encoded_transaction.hash() ) )
print( "Remaining signatures with card:", global_counter )
print( f"Remaining signatures with key {key_id}:", counter )
print( "Signature (hex):" + signature.hex() )
print()
# Extract r and s from the signature
try:
r, s = get_signature_components( signature )
except:
print( "Invalid signature components!" )
raise SystemExit()
# determine the signature prefix value
v = get_signature_prefix( ( r, s ), inf_card_addr, bytes( unsigned_encoded_transaction.hash() ), w3.eth.chainId )
# add the signature to the encoded transaction
signed_encoded_transaction = encode_transaction( unsigned_encoded_transaction, vrs=( v, r, s ) )
# send the signed, serialized transaction
tx_hash = w3.eth.sendRawTransaction( signed_encoded_transaction )
print( "Updating message..." )
# wait for transaction to be included on a block
tx_receipt = w3.eth.waitForTransactionReceipt( tx_hash )
print()
# print account balance
print( f'Balance of account {key_id} on Infineon card: { w3.fromWei( w3.eth.getBalance( inf_card_addr ), "ether" ) }' )
# execute the getMessage() contract function and print the result
message = contract.functions.getMessage().call()
print( "getMessage() returned:", f"\"{message}\"" )
As the output shows, the string we passed to the invoke_contract.py program on the command line is now stored in the contract’s message
variable.
~/blocksec2go_ethereum_examples$ python .\invoke_contract.py "Hello Ethereum!"
Found the specified reader and a Blockchain Security 2Go card!
Address of account 1 on Infineon card: 0xd612f147159200856f32Fbcfc3201bb75C511774
Balance of account 1 on Infineon card: 8.999647552
Remaining signatures with card: 999995
Remaining signatures with key 1: 99995
Signature (hex):304402205e657af24b1d4c6bb5971454abf2b9aa021e26e732936b332b50ea68222a0e78022074fda9ff08fccf57df408e3a54296b2fe002140403e8bd848d0f72a9388f84f8
Updating message...
Balance of account 1 on Infineon card: 8.999617489
getMessage() returned: "Hello Ethereum!"
Conclusion
Blockchain security is analogous to private key management. Loss or theft of one’s private keys results in outright loss of one’s account assets. A hardware-based key management approach avoids the common pitfalls that accompany traditional key management solutions, though, often at far greater implementation cost and complexity. Infineon’s Blockchain Security 2Go starter kit enables developers to implement a hardware-based security solution without being subjected to these hinderances. Private keys are what fundamentally define blockchain accounts, and because the private keys stored on these cards are securely generated and stored, the corresponding accounts can be considered secure as well. While not even the owner of the card may not be able to retrieve these private keys, they can still use the card’s API to sign transactions. These transactions could be payments, contract creations, or contract invocations; all of which would not be possible without being in possession of the card. If theft of the card is a concern, PIN authentication can be enabled to further bolster security.
References
[1] Infineon Technologies, “Blockchain/BlockchainSecurity2Go_UserManual.pdf,” 24 July 2019. [Online]. Available: https://github.com/Infineon/Blockchain/blob/master/doc/BlockchainSecurity2Go_UserManual.pdf. [Accessed 28 January 2020].
[2] A. M. Antonopoulos, “Mastering Bitcoin,” 5 October 2019. [Online]. Available: https://github.com/bitcoinbook/bitcoinbook. [Accessed 28 January 2020].
[3] A. M. Antonopoulos and G. Wood, “Mastering Ethereum,” 28 January 2020. [Online]. Available: https://github.com/ethereumbook/ethereumbook. [Accessed 28 January 2020].
[4] S. Evanczuk, “Build Security into Blockchain Applications - Part 1: How Blockchain Works and Uses Private Keys,” 8 October 2019. [Online]. Available: https://www.digikey.com/en/articles/techzone/2019/oct/build-security-into-blockchain-applications-part-1. [Accessed 28 January 2020].
[5] HID Global, “HID® OMNIKEY® 5422 Contact & Contactless Reader,” [Online]. Available: https://www.hidglobal.com/products/readers/omnikey/5422. [Accessed 28 January 2020].
[6] C. Research, “SEC 1: Elliptic Curve Cryptography,” 2000. [Online]. Available: http://www.secg.org/SEC1-Ver-1.0.pdf.
[7] The Internet Society, “RFC 3279 Algorithms and Identifiers for the Internet X.509 Public Key Infrastructure,” 2002. [Online]. Available: https://tools.ietf.org/html/rfc3279 .
[8] B. Badr, R. Horrocks and X. Wu, Blockchain By Example: A Developer’s Guide to Creating Decentralized Applications Using Bitcoin, Ethereum, and Hyperledger, Birmingham: Packt Publishing Ltd., 2018.