はじめに
本記事では、AWS KMS(Key Management Service)を使用してEthereumブロックチェーンとやり取りする方法について解説します。
AWS KMSは高いセキュリティレベルでの鍵管理を提供するサービスで、秘密鍵をローカルに保存することなく安全に電子署名を行うことができます。
今回は、JavaScript(TypeScript)とJavaの両方の環境でAWS KMSを使用する方法を紹介します。
この記事で得られること
- ethers.jsでのKMS連携実装
- Web3jでのKMS連携実装
ethers.js
ethers.jsは、Ethereumブロックチェーンとやり取りするためのJavaScriptライブラリです。
今回はv6に対応した最新版を使用します。
使用ライブラリ
メインライブラリ:ethers.js
AWS KMS連携ライブラリ:ethers-aws-kms-signer
このライブラリは、ethers.js V6に対応しており、AWS KMSとの連携を簡単に実装できます。
インストール方法
npm install "@cuonghx.gu-tech/ethers-aws-kms-signer"
実装例
以下は、AWS KMSを使用してAwsKmsSignerを作成する基本的な実装例です。
// AWS KMS Signerを作成
const kmsSigner = new AwsKmsSigner(
{
keyId: "<KEY_ID>",
region: "ap-northeast-1", // 東京リージョン
credentials: {
accessKeyId: "<AWS_ACCESS_KEY_ID>",
secretAccessKey: "<AWS_SECRET_ACCESS_KEY>",
},
},
ethers.provider as any
);
この実装により、秘密鍵をローカル環境に保存することなく、AWS KMSの安全な環境でトランザクションに署名することができます。
-
keyId:事前にAWS KMSで作成した楕円曲線暗号キーのIDを指定 -
region:KMSキーが作成されているAWSリージョンを指定 -
credentials:適切な権限を持つAWS認証情報を指定
Web3j
Java環境では、web3jライブラリを使用してEthereumブロックチェーンとやり取りします。
残念ながら、web3jには公式のAWS KMS連携ライブラリが存在しないため、独自に実装する必要があります。
以下では、web3jのECKeyPairクラスを拡張して、AWS KMSを使用した署名機能を実装する方法を紹介します。
実装の概要
KmsECKeyPairクラスは、web3jのECKeyPairクラスを継承し、署名処理をAWS KMSで行うように拡張したクラスです。
主な機能は以下の通りです:
- AWS KMSからの公開鍵取得
- KMSを使用したECDSA署名の生成
- Ethereum互換の署名形式への変換
実装コード
以下は、完全なKmsECKeyPairクラスの実装です:
package com.example.demo.logic;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import org.bouncycastle.asn1.ASN1BitString;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.web3j.crypto.CryptoUtils;
import org.web3j.crypto.ECDSASignature;
import org.web3j.crypto.ECKeyPair;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.model.GetPublicKeyRequest;
import software.amazon.awssdk.services.kms.model.GetPublicKeyResponse;
import software.amazon.awssdk.services.kms.model.MessageType;
import software.amazon.awssdk.services.kms.model.SignRequest;
import software.amazon.awssdk.services.kms.model.SignResponse;
import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;
/**
* AWS KMSを使用した楕円曲線暗号キーペア実装クラス
*
* Web3jのECKeyPairを拡張し、AWS KMS(Key Management Service)を使用して
* 楕円曲線暗号による電子署名を行います。
*
* 主な機能:
* - AWS KMSからの公開鍵取得
* - KMSを使用したECDSA署名の生成
*
* @author Generated
* @version 1.0
*/
public class KmsECKeyPair extends ECKeyPair {
// AWS認証情報とKMS設定
private String accessKey; // AWSアクセスキー
private String secretKey; // AWSシークレットキー
private String keyId; // KMSキーID
private Region region; // AWSリージョン
/**
* KMSを使用するECKeyPairのコンストラクタ
*
* @param accessKey AWSアクセスキー
* @param secretKey AWSシークレットキー
* @param keyId KMSキーID
* @param region AWSリージョン
* @throws IOException ASN.1解析時のI/Oエラー
*/
public KmsECKeyPair(String accessKey, String secretKey, String keyId, Region region) throws IOException {
super(BigInteger.ZERO,
getPublicKeyByKms(accessKey, secretKey, keyId, region));
this.accessKey = accessKey;
this.secretKey = secretKey;
this.keyId = keyId;
this.region = region;
}
@Override
public ECDSASignature sign(byte[] transactionHash) {
// KMSクライアントを作成
KmsClient kmsClient = createKmsClient(accessKey, secretKey, region);
// KMSでの署名リクエストを構築
// 参考: https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html
SignRequest signRequest = SignRequest.builder()
.keyId(keyId) // 署名するキーIDを指定
.message(SdkBytes.fromByteArray(transactionHash)) // 署名するメッセージダイジェストを指定
.messageType(MessageType.DIGEST) // 既にハッシュされているメッセージダイジェストには DIGESTを指定
.signingAlgorithm(SigningAlgorithmSpec.ECDSA_SHA_256) // 署名アルゴリズムに ECDSA_SHA_256 を指定
.build();
try {
// KMSで署名実行
SignResponse signResponse = kmsClient.sign(signRequest);
// 署名結果を取得(DER形式)
byte[] signature = signResponse.signature().asByteArray();
// DER形式の署名からECDSASignatureオブジェクトを作成
ECDSASignature ecdsaSignature = CryptoUtils.fromDerFormat(signature);
// 署名をcanonical形式に変換(Ethereumでは低いS値が必要)
return ecdsaSignature.toCanonicalised();
} finally {
// KMSクライアントを閉じる
kmsClient.close();
}
}
/**
* KMSクライアントを作成するユーティリティメソッド
* プロキシ設定を含むHTTPクライアントを使用してKMSクライアントを構築します。
*
* @param accessKey AWS アクセスキー
* @param secretKey AWS シークレットキー
* @param region AWS リージョン
* @return 構築されたKmsClientインスタンス(使用後は必ずclose()メソッドで閉じてください)
*/
private static KmsClient createKmsClient(String accessKey, String secretKey, Region region) {
// 認証情報を作成
AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey);
// HTTPクライアントを設定
SdkHttpClient httpClient = ApacheHttpClient.builder()
.connectionTimeout(Duration.ofSeconds(30))
.socketTimeout(Duration.ofSeconds(60))
.build();
// KMSクライアントを構築
return KmsClient.builder()
.region(region)
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.httpClient(httpClient)
.build();
}
/**
* AWS KMSから公開鍵を取得してBigInteger形式で返すメソッド
* DER形式の公開鍵をASN.1構文で解析し、楕円曲線暗号の公開鍵を抽出します。
*
* @param accessKey AWS アクセスキー
* @param secretKey AWS シークレットキー
* @param keyId KMSキーID
* @param region AWS リージョン
* @return 公開鍵のBigInteger表現
* @throws IOException ASN.1解析時のI/Oエラー
*/
private static BigInteger getPublicKeyByKms(String accessKey, String secretKey, String keyId, Region region)
throws IOException {
// KMSクライアントを作成
KmsClient kmsClient = createKmsClient(accessKey, secretKey, region);
try {
// KMSでの公開鍵取得リクエストを構築
// 参考: https://docs.aws.amazon.com/kms/latest/APIReference/API_GetPublicKey.html
GetPublicKeyRequest request = GetPublicKeyRequest.builder()
.keyId(keyId)
.build();
// KMSから公開鍵を取得
GetPublicKeyResponse response = kmsClient.getPublicKey(request);
byte[] publicKeyDer = response.publicKey().asByteArray();
// DER形式の公開鍵を解析してBigInteger形式の公開鍵を取得
try (ASN1InputStream asn1InputStream = new ASN1InputStream(publicKeyDer)) {
// ASN.1構文を解析
ASN1Primitive asn1Primitive = asn1InputStream.readObject();
// SubjectPublicKeyInfoを解析
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(asn1Primitive);
// 公開鍵のビット列を取得
ASN1BitString publicKeyBits = publicKeyInfo.getPublicKeyData();
byte[] publicKeyBytes = publicKeyBits.getBytes();
// 先頭の0x04バイトを除いた部分をBigIntegerとして設定
BigInteger publicKey = new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length));
return publicKey;
}
} finally {
// KMSクライアントを閉じる
kmsClient.close();
}
}
}
実装のポイント
1. 公開鍵の取得
AWS KMSから取得する公開鍵はDER形式でエンコードされているため、ASN.1構文を使用して解析し、楕円曲線暗号の公開鍵部分を抽出する必要があります。
2. 署名の生成
KMSで生成される署名はDER形式のため、web3jのCryptoUtils.fromDerFormat()メソッドを使用して ECDSASignature 形式に変換します。
3. Canonical形式への変換
Ethereumでは、署名のS値が低い値である必要があるため、toCanonicalised()メソッドを使用して適切な形式に変換します。
4. リソース管理
KMSクライアントは使用後に必ずclose()メソッドを呼び出してリソースを解放します。
使用方法
// KmsECKeyPairインスタンスを作成
KmsECKeyPair kmsKeyPair = new KmsECKeyPair(
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"arn:aws:kms:ap-northeast-1:123456789012:key/12345678-1234-1234-1234-123456789012",
Region.AP_NORTHEAST_1
);
// Web3jでの使用例
Web3j web3j = Web3j.build(new HttpService("http://localhost:8545"));
Credentials credentials = Credentials.create(kmsKeyPair);
まとめ
本記事では、AWS KMSを使用してEthereumブロックチェーンとの安全なやり取りを実現する方法について、JavaScript(TypeScript)とJavaの両方の実装例を紹介しました。
主なメリット
- セキュリティの向上: 秘密鍵をローカル環境に保存する必要がなく、AWS KMSの高いセキュリティレベルで鍵管理を行える
- コンプライアンス対応: 多くの規制要件に準拠したAWS KMSを活用することで、コンプライアンス要件を満たしやすくなる
- 運用の簡素化: 鍵のバックアップやローテーションなどの運用業務をAWSに委ねることができる
実装時の注意点
- AWS権限設定: KMSキーに対する適切な権限設定が必要
- ネットワーク接続: KMSとの通信が必要なため、ネットワーク接続が必須
- コスト管理: KMS使用料金とAPI呼び出し料金が発生することを考慮する
- パフォーマンス: ローカル署名と比較して処理時間が長くなる可能性がある
- キー削除の注意: AWS KMSからは秘密鍵を取得できないため、誤ってキーを削除した場合、関連するすべての暗号資産が永続的に失われる可能性があります
AWS KMSを活用することで、Ethereumアプリケーションのセキュリティを大幅に向上させることができます。