はじめに(Introduction)
BitcoinやEthereumでは署名にECDSA(楕円曲線DSA)を用いています。
そこで使用される、楕円曲線はSecp256k1と呼ばれるパラメータです。
AWS Key Management Service (AWS KMS) では、Secp256k1のECDSAを用いることが出来るのでこれをJavaを使用して署名、検証を行います。
サンプルで使用するライブラリとしては、Ethereumで利用されているWeb3jを使用します。
筆者は、AWSもEthereumも素人なので、そこら辺の間違いについてはご指摘いただけるとありがたいです。
この記事は後編です。
前編はこちら
実装(Programming)
JavaでKMSを利用して署名と検証を行います。
前編で取得した、IAMユーザの「アクセスキーID」と「シークレットアクセスキー」と
KSMで作成した鍵の「キーID」を使用します。
ライブラリの設定(Maven)
ライブラリをMaven(dependencies)で設定します。
上:EthereumクライアントのJavaライブラリ
下:AWSのKMSライブラリ
<!-- https://mvnrepository.com/artifact/org.web3j/core -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.9.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-kms -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-kms</artifactId>
<version>1.12.296</version>
</dependency>
システムプロパティの設定(オプション)
必要なシステムプロパティを設定します。
AWSKMS
の生成でSystemPropertiesCredentialsProvider
クラスを利用するので、以下の上2つのシステムプロパティが必須です。
Key | Value | Etc |
---|---|---|
aws.accessKeyId | IAMで作成したユーザーの「アクセスキーID」 |
SystemPropertiesCredentialsProvider クラスで必須 |
aws.secretKey | IAMで作成したユーザーの「シークレットアクセスキー」 |
SystemPropertiesCredentialsProvider クラスで必須 |
AWSKMS
KMSにアクセスするためのインターフェースを作成します。
サンプルコードでは認証にSystemPropertiesCredentialsProvider
を利用している為、前述のシステムプロパティ設定が必要ですが、別の認証プロバイダーを利用する場合は必要ありません。
リージョンは、KMSの「キーの作成」で「東京」リージョンを指定しているのでそれを設定します。
public AWSKMS getAwskms() {
AWSKMS awskms = AWSKMSClientBuilder.standard()
.withCredentials(new SystemPropertiesCredentialsProvider()) // AWSKMS認証
.withRegion(Regions.AP_NORTHEAST_1) // リージョン:アジアパシフィック (東京) ap-northeast-1
.build();
return awskms;
}
公開鍵取得(GetPublicKey)
キーの作成で取得した「キーID」の公開鍵を取得します。
「キーID」を指定して、GetPublicKeyを行うと、「DER-encoded X.509 public key」が取得できます。
Javaでは、SubjectPublicKeyInfo
オブジェクトが用意されているのでそれを利用します。
KMSからは非圧縮の公開鍵が取得できますが、仕様では圧縮された公開鍵でも良さそうなので、一度、Secp256k1の公開鍵に変換してから再度非圧縮の公開鍵を取得しています。
また、Web3j(Ethereum)の公開鍵は非圧縮の公開鍵の先頭(0x04
)を除いた整数(BigInteger
)なのでそれを生成して返します。
private static final X9ECParameters ECP = CustomNamedCurves.getByName("secp256k1");
public BigInteger getPublicKey(AWSKMS awskms, String keyId) throws Exception {
// PublickKey取得
GetPublicKeyRequest request = new GetPublicKeyRequest()
.withKeyId(keyId); // KMSのKeyIdを設定
GetPublicKeyResult result = awskms.getPublicKey(request);
// >>> Log
System.out.println(result.getKeySpec());
System.out.println(result.getKeyUsage());
// <<< Log
// PublickKeyのデータ取得
ByteBuffer buffer = result.getPublicKey();
BigInteger publicKey = null;
// ASN1形式から公開鍵を取得
try (ASN1InputStream inputStream = new ASN1InputStream(buffer.array())) {
ASN1Primitive primitive = inputStream.readObject();
if (primitive instanceof ASN1Sequence) {
// 公開鍵のオブジェクト作成
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(primitive);
DERBitString bitString = publicKeyInfo.getPublicKeyData();
// Secp256k1の点(公開鍵)に変換
ECCurve curve = ECP.getCurve();
ECPoint point = curve.decodePoint(bitString.getOctets());
byte[] bs = point.getEncoded(false);
// Web3jのPublicKey形式に変換(非圧縮形式から先頭の0x04を除いたBigInteger)
publicKey = new BigInteger(1, Arrays.copyOfRange(bs, 1, bs.length));
}
}
return publicKey;
}
署名(Sign)
キーの作成で取得した「キーID」で署名値を取得します。
ECDSAの場合、メッセージのハッシュ値を利用して署名値を求めます。
EthereumであればKeccak-256、BitcoinであればダブルSHA256(SHA256を2回)だったりします。
したがって、パラメータにはメッセージハッシュを指定する事にしました。
KMSはMessageType
にDIGEST
を設定することで、Message
にメッセージハッシュを設定することができます。
SigningAlgorithm
にはをECDSA_SHA_256
を設定します。(SHA_256が入っているのが気持ち悪い・・・)
ちなみに、MessageType
にDIGEST
を設定しないと、Message
をSHA256するようです。
Message
の最大値は4096バイトです。
KMSからの署名値は「DER-encoded」フォーマットなので変換します。
署名値には、$r$と$s$が含まれています。ECDSAでは$s$は$-s$としても署名値としては成立します。
EthereumやBitcoinではこの$s$を値の小さい方だけが有効としている為、修正が必要です。
(有限体なので、$s$も$-s$も正の整数となり、小さい方は位数の半分より下となります。)
$r$は楕円曲線上の点における$x$座標なので、$y$座標は2つ存在します。
Ethereumではこの$y$がどちらかを決定する必要があります。(厳密に言うと最大で4パターンある)
したがって、Ethereumの署名データに変換します。
public SignatureData sign(AWSKMS awskms, String keyId, BigInteger publicKey, byte[] messageHash) {
// AWSのKMSに署名リクエスト
SignRequest request = new SignRequest()
.withKeyId(keyId) // 署名するキーIDを設定
.withMessage(ByteBuffer.wrap(messageHash)) // Messageにメッセージハッシュを設定
.withMessageType(MessageType.DIGEST) // MesssageTypeにDIGESTを設定するとMessageはハッシュ値で送信可能
.withSigningAlgorithm(SigningAlgorithmSpec.ECDSA_SHA_256); // アルゴリズムにはECDSA SHA256を設定
SignResult result = awskms.sign(request);
// DERフォーマットを署名値に変換
ECDSASignature signature = CryptoUtils.fromDerFormat(result.getSignature().array());
// >>> Log
System.out.println(signature.r.toString(16));
System.out.println(signature.s.toString(16));
// <<< Log
// sの修正
signature = signature.toCanonicalised();
// 署名値をEthereum用の署名データに変換
SignatureData signatureData = Sign.createSignatureData(signature, publicKey, messageHash);
// >>> Log
System.out.println(Hex.toHexString(signatureData.getR()));
System.out.println(Hex.toHexString(signatureData.getV()));
System.out.println(Hex.toHexString(signatureData.getS()));
// <<< Log
return signatureData;
}
検証(Verify)
キーの作成で取得した「キーID」で検証を行います。
署名と同様に、メッセージハッシュをパラメータとします。
Signature
には、Ethereumの署名値データからECDSAの署名値に変換しそれをDERエンコードしたものを設定します。
public Boolean verify(AWSKMS awskms, String keyId, BigInteger publicKey, SignatureData signatureData,
byte[] messageHash) {
// ECDSAの署名値を生成
ECDSASignature ecdsaSignature = new ECDSASignature(
new BigInteger(1, signatureData.getR()),
new BigInteger(1, signatureData.getS()));
VerifyRequest verifyRequest = new VerifyRequest()
.withKeyId(keyId) // 署名するキーIDを設定
.withMessage(ByteBuffer.wrap(messageHash)) // Messageにメッセージハッシュを設定
.withMessageType(MessageType.DIGEST) // MesssageTypeにDIGESTを設定すると
.withSignature(ByteBuffer.wrap(CryptoUtils.toDerFormat(ecdsaSignature))) // 署名値をDERフォーマットで設定
.withSigningAlgorithm(SigningAlgorithmSpec.ECDSA_SHA_256); // アルゴリズムにはECDSA SHA256を設定
VerifyResult verifyResult = awskms.verify(verifyRequest);
return verifyResult.isSignatureValid();
}
テスト
Ethereumのメッセージ署名をKMSで署名、検証をしてみます。
検証については、Web3jのECDSARECOVERでも検証してみます。
private void test() throws Exception {
System.out.println(">>> getAgetAwskms");
AWSKMS awskms = getAwskms();
System.out.println("<<< getAgetAwskms");
System.out.println(">>> getPublicKey");
BigInteger publicKey = getPublicKey(awskms, KEY_ID);
String addr = Keys.getAddress(publicKey);
System.out.println(addr);
System.out.println("<<< getPublicKey");
System.out.println(">>> sign");
byte[] message = "TEST".getBytes();
byte[] messageHash = Sign.getEthereumMessageHash(message);
SignatureData signatureData = sign(awskms, KEY_ID, publicKey, messageHash);
System.out.println("<<< sign");
System.out.println(">>> verify");
Boolean result = verify(awskms, KEY_ID, publicKey, signatureData, messageHash);
System.out.println(result);
System.out.println("<<< verify");
// ECDSARECOVER
System.out.println(">>> ECDSARECOVER");
BigInteger recover = Sign.signedPrefixedMessageToKey(message, signatureData);
String rAddr = Keys.getAddress(recover);
System.out.println(rAddr);
System.out.println(addr.equals(rAddr));
System.out.println("<<< ECDSARECOVER");
}
s
がca4ceabdaf40d36b3bb49a8841b8eb6bd6e4141680a12248bbec3dbd90e0ab82
から35b3154250bf2c94c44b6577be471492e3cac8d02ea77df303e620cf3f5595bf
に変更されていますが、KMSの検証でも成功となっています。
また、ECDSARECOVERでも問題なく検証できています。
>>> getAgetAwskms
<<< getAgetAwskms
>>> getPublicKey
ECC_SECG_P256K1
SIGN_VERIFY
21fa0047e37ad5b9474bd29e8626f0b3a200d24a
<<< getPublicKey
>>> sign
8503b522859e553fa29089ac6cfe4fc296dd315d36f4338487e9e5cd31de1735
ca4ceabdaf40d36b3bb49a8841b8eb6bd6e4141680a12248bbec3dbd90e0ab82
8503b522859e553fa29089ac6cfe4fc296dd315d36f4338487e9e5cd31de1735
1c
35b3154250bf2c94c44b6577be471492e3cac8d02ea77df303e620cf3f5595bf
<<< sign
>>> verify
true
<<< verify
>>> ECDSARECOVER
21fa0047e37ad5b9474bd29e8626f0b3a200d24a
true
<<< ECDSARECOVER
まとめ(Conclusion)
無事、KMSの署名と検証をJavaから使用する事ができました。
これをやる前はメッセージハッシュをどうやるんだろうと思っていましたが、メッセージハッシュを送れるオプションがあって良かったです。
(ただ、アルゴリズムにSHA256が入っているのが気になるけど・・・)
疑問として、KMSの検証を使う場面はあるのだろうか?と思いました。
基本的に検証は署名者以外が使用する場合が大半だと思うのですが、自身の署名を検証する場面が思いつかない・・・
本記事ではEthereumのライブラリを利用していますが、筆者はBitcoin派です。