AWS Database Blog
Use Key Management Service (AWS KMS) to securely manage Ethereum accounts: Part 2
Ethereum is a popular public blockchain that makes it possible to create unstoppable applications in a permissionless fashion. It’s available to every user that has an Ethereum account. These Ethereum accounts consist of a private and an associated public key.
The main challenge as a user participating in a public blockchain such as Ethereum is safely managing the blockchain credentials. Because an externally owned Ethereum account is required to approve transactions, including funds transfers and other sensitive operations, its key material must be carefully safeguarded.
In some fully decentralized applications, the user is expected to manage their own key material. There are other applications, however, where it may desirable to entrust the management of key material to an external process or service, such as when the key material is frequently needed even when the user isn’t available. This is a common requirement for token staking and other modern blockchain applications.
This is the second of a two-post series about how to use AWS Key Management Service (AWS KMS) to manage Ethereum accounts.
The first post discussed how to do the following:
- Use the AWS Cloud Development Kit (AWS CDK) to define AWS infrastructure
- Apply custom configuration to AWS CDK objects
- Use a Docker-based build for AWS Lambda functions and integrate it into the AWS CDK
- Create signed Ethereum transactions based on the AWS customer master key (CMK)
- Integrate the presented sample solution with an Amazon Managed Blockchain Ethereum Rinkeby testnet node.
This post discusses the following:
- How Ethereum signatures work
- How to use the AWS CMK to create an Ethereum public address
- How to create Ethereum offline signatures using a CMK and make them portable
The third post in this series includes a deep dive into the Ethereum Improvement Proposal 1559 (EIP-1559) and explains how EIP-1559 transactions can be signed using AWS KMS.
Prerequisites
To follow along with this post, we recommend reading the first post and deploying the described AWS CDK-based solution.
To just make the source code available to your local system, you have to clone the repository from GitHub:
Lambda function (Ethereum key calculation)
The entire key handling logic is located in the eth-kms-client
Lambda function. The source code to the Lambda function is located in the /aws-kms-ethereum-accounts/aws_kms_lambda_ethereum/_lambda/functions/eth_client
folder.
Opening the lambda_function.py
gives you an overview of how the requests are being handled. The lambda_handler(event, context)
function expects a JSON request with an operation
parameter defined. Based on this operation parameter, the handler runs the requested logic.
In this example, status
and sign
are supported:
If you examine the implementation of the sign
operation more closely, the following steps are being run:
Let’s go over these steps in more detail.
Because the AWS KMS-CMK instance is being created at the beginning, you can’t have a fixed reference to the key_id
. To solve this problem, the key_id
is passed to the Lambda function as a configuration parameter in form of an environment variable using KMS_KEY_ID
as the variable name.
Lambda stores environment variables securely by encrypting them at rest. See the following code:
The Lambda function itself is not tied to a single Ethereum account. It can be extended to support multiple accounts by securely providing key_ids
to different AWS KMS-CMK instances within the AWS account. The destination AWS KMS-CMK instance would then need to be configured dynamically for each signing request. AWS Systems Manager Parameter Store should be used to securely store parameters and secrets. For more information see Sharing Secrets with AWS Lambda Using AWS Systems Manger Parameter Store.
The values for dst_address
, amount
, and nonce
have to be passed using the external event triggering the Lambda function as described at the beginning:
The CMK public key is downloaded using the passed key_id
. The implementation of the get_kms_public_key()
method is located in the lambda_helper.py
together with the lambda_function.py
file. See the following code:
As you can see, the download step consists of the initialization of a new Python Boto3 client for the kms
service. This client is then used to run the get_public_key()
API call using the key_id
that was passed. This step requires the kms:GetPublicKey
permission on the CMK resource. See the following code:
Based on the public key that has been downloaded from the CMK instance, we can now calculate the checksum form of the public Ethereum address.
First, we need to decode the public key based on the underlying ASN.1 schema. This schema definition is assigned to the SUBJECT_ASN
variable. We use the asn1tools
package in this example to compile the schema and decode the key:
The schema definition can be found in IETF RFC5280If the decode step was successful, the raw public key can be accessed using a Python dictionary using subjectPublicKey
as the key value.
It’s important to point out that according to IETF RFC5280, “the first octet of the OCTET STRING indicates whether the key is compressed or uncompressed. The uncompressed form is indicated by 0x04 and the compressed form is indicated by either 0x02 or0x03.”
Now the Keccak-256 hash value has to be taken from the raw public key. Furthermore, the Ethereum public address is defined to be the last 20 bytes (least signification bytes) of the hash value as stated in Chapter 4 of Mastering Ethereum.
Because the Python web3 library is being used in this example, we recommend using the provided Keccak method: w3.keccak()
.
After attaching the 0x
prefix at the beginning of the hex string, we have successfully calculated our Ethereum public address.
The last step is to convert the address into a checksum address. Checksum addresses have been specified in EIP-55 and provide a basic protection against typos and copy/paste errors in Ethereum address handling.
The conversion is performed using the w3.toChecksumAddress()
method.
We can now use this address to, for example, send funds to the CMK-based Ethereum account. These are then reflected in the associated state of the calculated addresses on the Ethereum blockchain.
get_tx_params()
returns a Python dictionary containing the transaction-related parameters like the amount of ether to send, the destination address, and the nonce
value, which acts as a replay protection:
assembe_tx()
is called to assemble and to sign the transaction:
In detail, the assemble_tx()
method (see the following code) consists of four high-level steps:
- tx_usnsigned – An unsigned transaction is created
- tx_sig – The hash value of the unsigned transaction is signed using AWS KMS and the resulting signature is decoded to extract and validate the values
r
ands
- tx_eth_recovered_pub_addr – A missing signature parameter
v
is calculated - tx_encoded – A signed raw transaction is being assembled and returned in a serialized form
Let’s walk through these four steps in detail.
- An unsigned transaction is created using the web3
serializable_unsigned_transaction_from_dict()
method:
- The hash value of the unsigned transaction is signed using AWS KMS:
Looking into the find_eth_signature()
method, you can see that the signature created by the CMK is returned in an ASN.1 schema. This schema needs to be decoded to get access to the values r
and s
. The schema definition for ECDSA signatures can be found in RFC3279 section 2.2.3.
With regards to the signing operation sign_kms()
, it’s important to point out that the returned ECDSA signature is different every time it’s calculated, even though the same payload is being used. The reason for that is because AWS KMS doesn’t use Deterministic Digital Signature Generation (DDSG) and certain parameters in the signature calculation process are chosen random, namely the k-value.
The consequence of using random parameter k
for the signature calculation is that the returned Ethereum signature is different every time, even using the same payload, as mentioned already.
After we extract r
and s
successfully, we have to test if the value of s
is greater than secp256k1n/2
as specified in EIP-2 and flip it if required.
The constant SECP256_K1_N
represents the max value for s defined for the particular elliptic curve, as specified in Standards for Efficient Cryptography.
- The missing parameter
v
is being recovered using theAccount.recoverHash()
function of theeth_account
Python package.v
is also referred to as the recovery parameter.
This parameter is important because Ethereum determines the sender’s public address based on the signature parameters r
, s
, and v
. The function to determine the sender’s address from an assembled and signed transaction is important, so that an Ethereum peer can, for example, check if an account has sufficient funds for the gas costs associated with an Ethereum transaction.
Bitcoin, for example, uses the same cryptographic parameters but doesn’t require parameter v
because it attaches the sender’s public address to the transaction. Ethereum avoids attaching the sender’s public address to save some bytes on the transaction size.
As stated in EIP-155, v
is supposed to be determined based on the ChainID
parameter to prevent replay attacks between different Ethereum networks like the Ethereum main net and the Rinkeby testnet.
The proposed way to determine v
is: “v of the signature MUST be set to {0,1} + CHAIN_ID * 2 + 35, where {0,1} is the parity of the y value of the curve point for which r is the x-value in the secp256k1 signing process.”
Because this described approach relies on the CMK-based signature, we can’t calculate v
as specified.
Instead, we can use a fallback mechanism specified in EIP-155. Here it’s stated that “the currently existing signature scheme using v = 27 and v = 28 remains valid and continues to operate under the same rules as it did previously.”
Because the public address, the hash value of the payload, and the values r
and s
are known, we can calculate the missing parameter v
.
The eth_account
Python package provides a function Account.recoverHash()
that consumes a message hash and the parameters v
, r
, and s
:
As shown in the preceding code, we can run the recoverHash()
twice with the values v=27
and v=28
. If there is a collision with the passed Ethereum checksum address that was calculated earlier, the right value v has been determined.
If there is no match, something is wrong with either the previously calculated signatures or the payload.
- The last step consists of assembling a signed raw transaction, based on the
unsigned_transaction
value and the calculated signature valuesr
,s
, andv
- . To do so, we can use the
encode_transaction()
method provided by theeth_account
Python package. This method returns a serialized version of the transaction object:
The last step in the Lambda function consists of decoding the signed and serialized Ethereum transaction as a hexadecimal string and returning it embedded in a Python dictionary, which is returned as a JSON object by Lambda:
The hexadecimal string format is required to prevent encoding and escaping problems while moving the raw transaction payload around.
This signed transaction can now be used using the Ethereum JSON RPC eth_sendRawTransaction method, for example together with the Managed Blockchain for Ethereum nodes.
Keep in mind that the calculated AWS KMS CMK-based Ethereum address per default doesn’t have any funds associated with it, so it needs to be funded first.
Clean up
To avoid incurring future charges, delete the resources using the AWS CSK with the following command:
You can also delete stacks deployed by the AWS CDK via the AWS CloudFormation console.
Conclusion
This series of posts discussed managing Ethereum key material using a CMK and Lambda.
In the first post, we talked about how to configure and deploy the required services using the AWS CDK. we explained how to configure an Ethereum-compatible CMK and how to extend the solution to send transactions to the Ethereum Rinkeby testnet using a Managed Blockchain Ethereum node.
In this second post, we explained the inner workings of the Ethereum signature process and showed how to use the created CMK resource to derive a public Ethereum address, how to create valid Ethereum offline signatures, and how to make these signatures portable.
About the Author
David Dornseifer is a Blockchain Architect with the Amazon ProServe Blockchain team. He focuses on helping customers design, deploy and scale end-to-end Blockchain solutions.