ノードの情報は必ずしも正しいとは限らない
私たちはトランザクションを取得する際,ノードから情報を取得します.当然,アプリケーションを作成する場合はノードからの情報取得が必須になります.多くの場合,自分の信頼しているノードから情報を取得すると思います.しかしながら,ノードはWebサーバーでありハッキングされることも十分にあり得ます.そして,ハッキングされたノードが不正なトランザクションを配信する可能性もあります.
不正なトランザクションはアプリケーションに様々な不具合を引き起こしてしまいます.例えば取引所が入金の監視に使っているノードが不正なトランザクションを配信するようになれば不正入金を引き起こすかもしれません.これを防ぐには,ノードが正しい情報を配信しているかどうか確認が必要です.
そこで今回はトランザクションに含まれる署名の検証を行ってみました.
今回はトランザクションの署名のみを検証しています.実際に正当なトランザクションであるかを検証するには,チェーンにトランザクションが承認されているかなど,更に他の検証が必要です.
トランザクションの作成
まず正しい署名済みトランザクションを作成してみます
const account = xym.Account.createFromPrivateKey("***",152);
const tx = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
listenAccount.address,
[],
xym.PlainMessage.create(""),
xym.NetworkType.TEST_NET
).setMaxFee(100);
const stx = account.sign(tx,networkGenerationHash);
次に,署名済みトランザクションの内容を変えてみます.今回はmosaicの部分を操作し,モザイクの送金が無かった上のトランザクションを9999.999999XYM送金するように改造してみます
const stx = xym.TransferTransaction.createFromPayload(st.payload);
const badTx = new xym.TransferTransaction(
stx.networkType ,
stx.version,
stx.deadline,
stx.maxFee,
stx.recipientAddress,
[new xym.Mosaic(currencyMosaicId, xym.UInt64.fromUint(9999999999))],
stx.message,
stx.signature,
stx.signer,
stx.transactionInfo
);
mosaicの部分だけ偽造が完了しました.ではこれらを検証していきましょう.
検証する
署名済みトランザクションにはpayloadが含まれており,実際にアナウンスする際はこのoayloadがノードにputされています.ノードはこのpayloadを検証することでトランザクションが不正なものでないか判定しています.
javascriptのSDKを追ったところ,payloadはhexを文字列したものになっていました.これをUint8Arrayに変換したことろ次のような構成になっていました.あくまでトランザクションの検証が目的なので深い部分まではトランザクションの構造などは調査出来ていません(今後変更の可能性はあります)
(8バイト(サイズなど?)+(64バイト(署名))+(32バイト(署名者公開鍵))+(4バイト(0が4つ))+(トランザクションの中身)
例えばpayloadが
A100000000000000C01ADC86B782F8F26C6ABA9E9E01F3EC7C272C02F3A31D73A54965384768AB75EF204A2F2CD83AAA4096EBC91F2768845F6D0F7B41BF905F969928A58EFCC407C50D9B1B2FF3E8E17854FDEEF3E89D4861C25C624F0F05EFE6514DF3BDC0A1DE0000000001985441E43E00000000000001AACB1D0200000098FEEEE70A6A029898E98EBFA04C004922766A15E9A51FF2010000000000000000
の場合
署名
C01ADC86B782F8F26C6ABA9E9E01F3EC7C272C02F3A31D73A54965384768AB75EF204A2F2CD83AAA4096EBC91F2768845F6D0F7B41BF905F969928A58EFCC407
署名者公開鍵
C50D9B1B2FF3E8E17854FDEEF3E89D4861C25C624F0F05EFE6514DF3BDC0A1DE
中身
01985441E43E00000000000001AACB1D0200000098FEEEE70A6A029898E98EBFA04C004922766A15E9A51FF2010000000000000000
となります
SDKを追ったところ,署名はgenerationHashとトランザクションの内容をくっつけたものに対してed25519で行っていました.この内容に対して公開鍵と署名の検証を行えばよさそうです.
検証にはsymbol-sdkで署名の際に使用されているライブラリ:tweetnaclを使用しました.symbol-sdkをインストールすると一緒にインストールされるので追加インストールは不要です
function validatePayload(payload){
try{
//payloadをuint8Arrayへ
const uint8ArrayPayload = bufferToUint8Array(Buffer.from(payload, "hex"));
//署名を切り出す
const signature = uint8ArrayPayload.slice(8, 8 + 64);
//署名者公開鍵を切り出す
const signerPublicKey = uint8ArrayPayload.slice(8 + 64, 8 + 64 + 32);
//
const transactionWithoutHeader = uint8ArrayPayload.slice(8 + 64 + 32 + 4, )
//generationHashをuint8arrayに
const uint8arrayGenerationHash = bufferToUint8Array(Buffer.from(networkGenerationHash, "hex"));
//generationHashとTXをくっつける
const validateData = new Uint8Array([...uint8arrayGenerationHash, ...transactionWithoutHeader]);
if (nacl.sign.detached.verify(validateData, signature, signerPublicKey)) return true;
else return false;
}catch{
return false;
}
}
コード全文です
const xym = require('symbol-sdk');
const nacl = require("tweetnacl");
var networkGenerationHash = '';
var epochAdjustment = '';
var repo;
var transactionHttp;
var receiptRepo;
var currencyMosaicId;
var node;
var networkType = xym.NetworkType.TEST_NET;
const privateKey = '***';
//送信元アカウント
const listenAccount = xym.Account.createFromPrivateKey(
privateKey,
networkType,
);
(async () => {
node = 'https://sym-test-09.opening-line.jp:3001';
repo = new xym.RepositoryFactoryHttp(node);
await getInfo()
simpleTx()
})();
async function getInfo() {
feemultiplier = 100;
epochAdjustment = await repo.getEpochAdjustment().toPromise();
networkGenerationHash = await repo.getGenerationHash().toPromise();
currencyMosaicId = (await repo.getCurrencies().toPromise()).currency.mosaicId.toHex();
}
function simpleTx(){
const tx = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
listenAccount.address,
[],
xym.PlainMessage.create(""),
networkType
).setMaxFee(100);
const st = listenAccount.sign(tx, networkGenerationHash);
const stx = xym.TransferTransaction.createFromPayload(st.payload);
const badTx = new xym.TransferTransaction(
stx.networkType ,
stx.version,
stx.deadline,
stx.maxFee,
stx.recipientAddress,
[new xym.Mosaic(currencyMosaicId, xym.UInt64.fromUint(9999999999))],
stx.message,
stx.signature,
stx.signer,
stx.transactionInfo
);
console.log("plainTx", validatePayload(tx.serialize()));
console.log(tx);
console.log("signedTx:", validatePayload(st.payload));
console.log(stx);
console.log("badTx:",validatePayload(badTx.payload));
console.log(badTx);
}
function bufferToUint8Array(buf) {
const view = new Uint8Array(buf.length);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return view;
}
function validatePayload(payload){
console.log(payload)
try{
//payloadをuint8Arrayへ
const uint8ArrayPayload = bufferToUint8Array(Buffer.from(payload, "hex"));
//署名を切り出す
const signature = uint8ArrayPayload.slice(8, 8 + 64);
//署名者公開鍵を切り出す
const signerPublicKey = uint8ArrayPayload.slice(8 + 64, 8 + 64 + 32);
//
const transactionWithoutHeader = uint8ArrayPayload.slice(8 + 64 + 32 + 4, );
//generationHashをuint8arrayに
const uint8arrayGenerationHash = bufferToUint8Array(Buffer.from(networkGenerationHash, "hex"));
//generationHashとTXをくっつける
const validateData = new Uint8Array([...uint8arrayGenerationHash, ...transactionWithoutHeader]);
if (nacl.sign.detached.verify(validateData, signature, signerPublicKey)) return true;
else return false;
}catch{
return false;
}
}
これを実行すると改ざんしたトランザクションと署名前のトランザクション(署名のないトランザクション)が検証に失敗し,改ざんまえの署名済みトランザクションのみが検証に成功することがわかります.
実際には
今回は署名が正しいか確認を行いましたが,実際にチェーンのトランザクションを検証する場合には署名の検証だけでは不十分です.なぜなら,署名されたトランザクションがブロックチェーンに承認されている,有効となっているかは別であるからです.また,署名を生成するだけであれば残高の有無にかかわらず可能です.例えば,アドレスの残高が0XYMであっても1000000XYMを送金する署名を生成したりすることはできてしまいます.署名済みトランザクションのみを信頼しないよう注意してください.
ネットワークを介さずに署名をうけとる(オフライン取引)の場合であればノードの情報を利用できないため,この検証のみが活用できると思います.ただし,受け取ったオフライントランザクションをネットワークにアナウンスする前に別に生成したトランザクションによってアドレスの残高を抜く操作は可能なので,検証したとしても必ずしもトランザクションの承認が保証されていないことに注意する必要があります.