Amazon Web Services ブログ

Ethereumアカウントを AWS Key Management Service を活用して安全に管理する – Part 2

このブログは、Use Key Management Service (AWS KMS) to securely manage Ethereum accounts: Part 2 を翻訳したものです。

Ethereumは、止まらないアプリケーションを作ることが可能な人気のパーミッションレス型のパブリックブロックチェーンです。Ethereum アカウントを持つすべてのユーザーが利用できます。これらの Ethereum アカウントは、秘密鍵と関連する公開鍵で構成されています。

Ethereum などのパブリック ブロックチェーンに参加するユーザーとしての主な課題は、ブロックチェーンの資格情報を安全に管理することです。外部所有の Ethereum アカウント(EOA)は、資金移動やその他の機密操作を含むトランザクションを承認する必要があるため、そのキー マテリアルは慎重に保護する必要があります。

一部の完全に分散化されたアプリケーションでは、ユーザーが独自のキー マテリアルを管理する必要があります。ただし、キー マテリアルの管理を外部のプロセスまたはサービスに委ねるのが望ましいアプリケーションは他にもあります。たとえば、ユーザーが利用できないときでもキー マテリアルが頻繁に必要になる場合などです。これは、トークンのステーキングやその他の最新のブロックチェーン アプリケーションの一般的な要件です。

これは、AWS Key Management Service (AWS KMS) を使用してEthereumアカウントを管理する方法についての 2 回投稿されたシリーズの第 2 回目です。

第1回目の投稿では、次の方法について説明しました。

  • AWS Cloud Development Kit(AWS CDK)を使用してAWSインフラストラクチャを定義する
  • AWS CDK オブジェクトにカスタム設定を適用する
  • AWS Lambda 関数に Docker ベースのビルドを使用して AWS CDK に統合する
  • AWS カスタマーマスターキー (CMK) に基づいて署名済みのEthereumトランザクションを作成する
  • 提示されたサンプルソリューションをAmazon Managed Blockchain Ethereum Rinkeby テストネットノードと統合します。

この投稿では、以下について説明します。

  • Ethereumの署名の仕組み
  • AWS CMK を使用してEthereumのパブリックアドレスを作成する方法
  • AWS CMK を使用してEthereumのオフライン署名を作成し、移植可能にする方法

このシリーズの 3 番目の投稿 には、Ethereum改善提案 1559 (EIP-1559) の詳細と、AWS KMS を使用して EIP-1559の トランザクションに署名する方法について説明しています。

前提条件

この記事を読み進めるには、初回の投稿 を読んで、説明されている AWS CDK ベースのソリューションをデプロイすることをお勧めします。

ソースコードをローカルシステムで使用できるようにするには、GitHub からリポジトリをクローンする必要があります。

git clone https://github.com/aws-samples/aws-kms-ethereum-accounts.git

Lambda関数(Ethereumキー計算)

キー処理ロジック全体はeth-kms-client の Lambda関数にあります。Lambda 関数のソースコードは /aws-kms-ethereum-accounts/aws_kms_lambda_ethereum/_lambda/functions/eth_client フォルダーにあります。

lambda_function.py を開くと、要求がどのように処理されているかの概要が表示されます。lambda_handler(event, context) 関数には、操作パラメーターが定義された JSON リクエストが必要です。このオペレーションパラメータに基づいて、ハンドラーは要求されたロジックを実行します。

この例では、statussign がサポートされています。


operation = event.get('operation')
    if not operation:
        raise ValueError('operation needs to be specified in request and needs to be eigher "status" or "send"')
	
	if operation == 'status':

    [...]

    eth_checksum_address = ...

    return {'eth_checksum_address': eth_checksum_address}


elif operation == 'sign':

    if not (event.get('amount') and event.get('dst_address') and event.get('nonce')):
        return {'operation': 'sign',
                'error': 'missing parameter - sign requires amount, dst_address and nonce to be specified'}
    [...]

    raw_tx_signed = ...

    return {"signed_tx": raw_tx_signed}

署名操作の実装をさらに詳しく調べると、次の手順が実行されています。


# get key_id from environment varaible key_id = os.getenv('KMS_KEY_ID')

# get destination address from send request dst_address = event.get('dst_address')

# get amount from send request amount = event.get('amount')

# nonce from send request nonce = event.get('nonce')

# download public key from KMS pub_key = get_kms_public_key(key_id)

# calculate the Ethereum public address from public key eth_checksum_addr = calc_eth_address(pub_key)

# collect raw parameters for Ethereum transaction tx_params = get_tx_params(dst_eth_addr=dst_address, amount=amount, nonce=nonce)

# assemble Ethereum transaction and sign it offline raw_tx = assemble_tx(tx_params=tx_params, params=params, eth_checksum_addr=eth_checksum_addr)

return {"signed_tx": raw_tx_signed}

これらの手順をさらに詳しく見ていきましょう。

AWS KMS-CMK インスタンスは最初に作成されるため、key_id への固定参照を持つことはできません。この問題を解決するには、KMS_KEY_ID を変数名として使用する環境変数の形式で、key_id を設定パラメータとして Lambda 関数に渡します。

Lambda は環境変数を保存時に暗号化して安全に保存します 。次のコードを参照してください。


# get key_id from environment variable key_id = os.getenv('KMS_KEY_ID')

ラムダ関数自体は、単一のEthereumアカウントに関連付けられていません。AWS アカウント内の異なる AWS KMS-CMK インスタンスにkey_ids を安全に提供することで、複数のアカウントをサポートするように拡張できます。送信先の AWS KMS-CMK インスタンスは、署名リクエストごとに動的に設定する必要があります。AWS Systems Manager パラメータストアは、パラメータとシークレットを安全に保存するために使用する必要があります。詳細については、「AWS Systems Manger Parameter Storeを使用した AWS Lambda とのシークレットの共有」を参照してください

冒頭で説明したように、dst_addressamountnonce の値は、Lambda 関数をトリガーする外部イベントを使用して渡す必要があります。


# get destination address from send request dst_address = event.get('dst_address')# get amount from send request # get amount from send request amount = event.get('amount')# nonce from send request # nonce from send request nonce = event.get('nonce')

CMK 公開鍵は、渡された key_id を使用してダウンロードされます。get_kms_public_key() メソッドの実装は、lambda_function.py ファイルとともに lambda_helper.py にあります。次のコードを参照してください。


# download public key from KMS

pub_key = get_kms_public_key(key_id)

ご覧のとおり、ダウンロード手順は kms サービス用の新しい Python Boto3 クライアントの初期化で構成されています。次に、このクライアントを使用して、渡された key_id を使用して get_public_key() API 呼び出しを実行します。このステップには、CMK リソースに対する kms:GetPublicKey 権限が必要です。次のコードを参照してください。


def get_kms_public_key(key_id: str) -> bytes: client = boto3.client('kms')

response = client.get_public_key( KeyId=key_id ) return response['PublicKey']

CMK インスタンスからダウンロードした公開鍵に基づいて、公開Ethereumアドレスのチェックサムフォームを計算できるようになりました。


# calculate the Ethereum public address from public key eth_checksum_addr = calc_eth_address(pub_key)

まず、基盤となる ASN.1 スキーマに基づいて公開鍵をデコードする必要があります。このスキーマ定義は、SUBJECT_ASN 変数に割り当てられます。この例では asn1tools パッケージを使用してスキーマをコンパイルし、キーをデコードします。


def calc_eth_address(pub_key) -> str:
	SUBJECT_ASN='''
	Key DEFINITIONS ::= BEGIN`

	SubjectPublicKeyInfo  ::=  SEQUENCE  {
		algorithm         AlgorithmIdentifier,
		subjectPublicKey  BIT STRING
	}

	AlgorithmIdentifier  ::=  SEQUENCE  {
		algorithm   OBJECT IDENTIFIER,
		parameters  ANY DEFINED BY algorithm OPTIONAL
	}
	END
	'''
	
	key = asn1tools.compile_string(SUBJECT_ASN)
	key_decoded = key.decode('SubjectPublicKeyInfo', pub_key)
	
	pub_key_raw = key_decoded['subjectPublicKey'][0]
	pub_key = pub_key_raw[1:len(pub_key_raw)]
	
	hex_address = w3.keccak(bytes(pub_key)).hex()
	eth_address = '0x{}'.format(hex_address[-40:])

	eth_checksum_addr = w3.toChecksumAddress(eth_address)
	
	return eth_checksum_addr

スキーマ定義は IETF RFC5280にあります。デコードステップが成功した場合、subjectPublicKeyをキー値として使用して、Pythonディクショナリを使用して元の公開鍵にアクセスできます。
IETF RFC5280によれば、「オクテットSTRINGの最初のオクテットは、キーが圧縮されているか圧縮されていないかを示す」ことを指摘することが重要です。圧縮されていないフォームは 0x04 で示され、圧縮されたフォームは 0x02 または 0x03 のいずれかで示されます。

ここで、Keccak-256 ハッシュ値は元の公開鍵から取得する必要があります。さらに、Ethereumのパブリックアドレスは、マスタリング Ethereum の第4章で述べられているように、ハッシュ値の最後の20バイト(最下位バイト)で定義されています。

この例では Python Web3 ライブラリが使用されているため、提供されている Keccak メソッド w3.keccak() を使用することをお勧めします。

16進文字列の先頭に0xプレフィックスを付けると、Ethereumのパブリックアドレスが正常に計算されました。

最後のステップは、アドレスをチェックサムアドレスに変換することです。チェックサムアドレスはEIP-55で指定されており、Ethereumアドレス処理におけるタイプミスやコピー/ペーストエラーに対する基本的な保護を提供します。

変換は w3.toChecksumAddress() メソッドを使用して実行されます。

これで、このアドレスを使用して、たとえば CMK ベースのEthereumアカウントに資金を送ることができます。これらは、Ethereumブロックチェーン上の計算されたアドレスの関連するステートに反映されます。

get_tx_params() は、送信するイーサの量、宛先アドレス、nonceなどのトランザクション関連のパラメータを含む Python の辞書型の戻り値を返します。これはリプレイ保護の役割を果たします。


# collect raw parameters for Ethereum transaction tx_params = get_tx_params(dst_eth_addr=dst_address, amount=amount, nonce=nonce) def get_tx_params(dst_eth_addr: str, amount: int, nonce: int) -> dict: transaction = { 'nonce': nonce, 'to': dst_eth_addr, 'value': w3.toWei(amount, 'ether'), 'data': '0x00', 'gas': 160000, 'gasPrice': '0x0918400000' } return transaction

assembe_tx() が呼び出され、トランザクションのアセンブルと署名が行われます。


# assemble Ethereum transaction and sign it offline``# assemble Ethereum transaction and sign it offline raw_tx_signed = assemble_tx(tx_params=tx_params, params=params, eth_checksum_addr=eth_checksum_addr)

詳しく説明すると、assemble_tx()メソッド (次のコードを参照) は 4 つの大まかなステップで構成されています。

  • tx_usnsigned 署名されていないトランザクションが作成されます
  • tx_sig 署名されていないトランザクションのハッシュ値が AWS KMS を使用して署名され、結果の署名がデコードされて値 rs が抽出および検証されます。
  • tx_eth_recovered_pub_addr 欠落している署名パラメーター v が計算されます
  • tx_encoded 署名された未処理トランザクションが組み立てられ、シリアル化された形式で返されます

def assemble_tx(tx_params: dict, params: EthKmsParams, eth_checksum_addr: str) -> bytes:
    tx_unsigned = serializable_unsigned_transaction_from_dict(tx_params)
    tx_hash = tx_unsigned.hash()
	
	tx_sig = find_eth_signature(params=params,
                            plaintext=tx_hash)
	
	tx_eth_recovered_pub_addr = get_recovery_id(tx_hash, tx_sig['r'], tx_sig['s'], eth_checksum_addr)
	
	tx_encoded = encode_transaction(tx_unsigned,
                                vrs=(tx_eth_recovered_pub_addr['v'], tx_sig['r'], tx_sig['s']))
								
	return tx_encoded

これら 4 つのステップを詳しく見ていきましょう。

  1. 署名のないトランザクションは web3 の serializable_unsigned_transaction_from_dict() メソッド を使用して作成されます。
  2. 
    tx_unsigned = serializable_unsigned_transaction_from_dict(transaction_dict=tx_params)
    
  3. 署名されていないトランザクションのハッシュ値は AWS KMS を使用して署名されます。

tx_sig = find_eth_signature(params=params,plaintext=tx_hash)

find_eth_signature() メソッドを見ると、CMK によって作成された署名が ASN.1 スキーマで返されていることがわかります。rs の値にアクセスするには、このスキーマをデコードする必要があります。ECDSA シグネチャのスキーマ定義は、RFC3279 セクション 2.2.3 にあります。

これら 4 つのステップを詳しく見ていきましょう。


SIGNATURE_ASN = '''
	Signature DEFINITIONS ::= BEGIN
	
	Ecdsa-Sig-Value  ::=  SEQUENCE  {
       r     INTEGER,
       s     INTEGER  }

	END
	'''
	signature_schema = asn1tools.compile_string(SIGNATURE_ASN)

	signature = sign_kms(params.get_ksm_key_id(), plaintext)

	# https://tools.ietf.org/html/rfc3279#section-2.2.3
	signature_decoded = signature_schema.decode('Ecdsa-Sig-Value', signature['Signature'])
	s = signature_decoded['s']
	r = signature_decoded['r']`

署名操作 sign_kms() に関しては、同じペイロードが使用されている場合でも、返される ECDSA 署名は計算されるたびに異なることに注意してください。その理由は、AWS KMS はDeterministic Digital Signature Generation (DDSG) を使用せず、署名計算プロセスの特定のパラメータ、つまり k 値がランダムに選択されるためです。

シグネチャの計算にランダムパラメーター k を使用した結果、すでに述べたように、返されるEthereumのシグネチャは、同じペイロードを使用していても毎回異なります。

rs を正常に抽出したら、s の値が EIP-2 で指定されている secp256k1n/2 より大きいかどうかをテストし、必要に応じて反転させる必要があります。


secp256_k1_n_half = SECP256_K1_N / 2

	if s > secp256_k1_n_half:
    	s = SECP256_K1_N - s`

定数 SECP256_K1_N は、Standards for Efficient Cryptography で指定されているように、特定の楕円曲線に定義された s の最大値を表します。

  1. 欠落しているパラメーター vAccount.recoverHash() 関数を使用して回復されます。Account.recoverHash() 関数は eth_account Python パッケージにあります。 v は回復パラメーターとも呼ばれます。

tx_eth_recovered_pub_addr = get_recovery_id(msg_hash=tx_hash,
				r=tx_sig['r'], 
    			s=tx_sig['s'], 
    			eth_checksum_addr=eth_checksum_addr)

Ethereumは署名パラメーター rsv に基づいて送信者のパブリックアドレスを決定するため、このパラメーターは重要です。たとえば、Ethereumピアが、Ethereumトランザクションに関連するガス代に十分な資金があるかどうかをEthereumのピアが確認できるように、アセンブルされ署名されたトランザクションから送信者のアドレスを決定する機能が重要です。

たとえば、Bitcoin は同じ暗号化パラメーターを使用しますが、トランザクションに送信者のパブリックアドレスを付与するため、パラメーター v は必要ありません。Ethereumは、トランザクションサイズをいくらか節約するために、送信者のパブリックアドレスを添付することを避けています。

EIP-155 で述べたように、vはEthereumメインネットやRinkebyテストネットなどの異なるEthereumネットワーク間のリプレイ攻撃を防ぐために、ChainID パラメータに基づいて決定されることになっています。

v を決定する方法として提案されているのは、「署名の v は {0,1} + CHAIN_ID * 2 + 35 に設定する必要があります。ここで {0,1} は曲線点の Y 値のパリティで、r は secp256k1 署名プロセスの X 値です。

ここで説明した方法はCMKベースの署名に依存しているため、vを指定どおりに計算することはできません。

代わりに、EIP-155 で指定されているフォールバックメカニズムを使用できます。ここでは、「v = 27 と v = 28 を使用する現在の署名スキームは引き続き有効であり、以前と同じルールの下で機能し続けている」と記載されています。

パブリックアドレス、ペイロードのハッシュ値、rs の値がわかっているため、欠落しているパラメーター v を計算できます。
eth_account Python パッケージには、メッセージハッシュとパラメーター vrs を使用する関数 Account.recoverHash() が用意されています。


def get_recovery_id(msg_hash, r, s, expected_eth_addr) -> dict:
	for v in [27, 28]:
	recovered_addr = Account.recoverHash(message_hash=msg_hash,
                                             vrs=(v, r, s))
											 
        if recovered_addr == expected_eth_addr:
               return {'recovered_addr': recovered_addr, 'v': v}
			   
	return {}

前のコードで示したように、値が v=27v=28 の場合、recoverHash() を 2 回実行できます。渡されたEthereumチェックサムアドレスと先に計算されたアドレスが衝突した場合は、正しい値 v が決定されています。

一致しない場合は、以前に計算された署名またはペイロードのいずれかに問題があります。

  1. 最後のステップでは、unsigned_transaction 値と計算された署名値 rsv に基づいて、署名された未処理トランザクションを組み立てます。
  2. そのためには、eth_account Python パッケージで提供されている encode_transaction() メソッドを使用できます。このメソッドは、トランザクションオブジェクトのシリアル化されたバージョンを返します。

tx_encoded = encode_transaction(unsigned_transaction=tx_unsigned,
vrs=(tx_eth_recovered_pub_addr['v'], tx_sig['r'], tx_sig['s']))

Lambda 関数の最後のステップは、署名されシリアル化された Ethereumトランザクションを 16 進文字列としてデコードし、それを Python ディクショナリに埋め込んで返すことです。これは Lambda によって JSON オブジェクトとして返されます。


return w3.toHex(tx_encoded)

未処理のトランザクションペイロードを移動する際のエンコードやエスケープの問題を防ぐには、16 進数の文字列形式が必要です。

この署名付きトランザクションは、Ethereum JSON RPC eth_sendRawTransactionメソッドを使用して、たとえばEthereumノード用のマネージドブロックチェーンとともに使用できるようになりました。

計算された AWS KMS CMK ベースの Ethereum アドレスにはデフォルトで資金がないため、最初に資金を調達する必要があることに注意してください。

クリーンアップ

今後課金されないようにするには、次のコマンドを使用して AWS CDK を使用してリソースを削除します。


cdk destroy

AWS CDK によってデプロイされたスタックは、AWS CloudFormation コンソールを使用して削除することもできます。

結論

この一連の投稿では、CMK と Lambda を使用してEthereumのキーマテリアルを管理する方法について説明しました。
最初の投稿 では、AWS CDK を使用して必要なサービスを設定およびデプロイする方法について説明しました。Ethereum互換の CMK を設定する方法と、マネージドブロックチェーンEthereumノードを使用してトランザクションを Ethereum Rinkeby テストネットに送信するようにソリューションを拡張する方法について説明しました。

この2つ目の投稿では、Ethereum署名プロセスの内部動作を説明し、作成したCMKリソースを使用してパブリックEthereumアドレスを導出する方法、有効なEthereumオフライン署名を作成する方法、およびこれらの署名を移植可能にする方法を示しました。

著者について

David Dornseiferは、Amazon ProServe ブロックチェーンチームのブロックチェーンアーキテクトです。彼は、顧客がエンドツーエンドのブロックチェーンソリューションを設計、展開、拡張するのを支援することに重点を置いています。

このブログは、ソリューションアーキテクトの渡邊英士が翻訳しました。