はじめに
カタパルトのDragonが公開されました。
変更箇所の一つに、リプレイプロテクションの実装があります。
チェンジログには、以下のような記載があります。
Prevent transactions from being replayed on different networks by prepending the network generation hash to transaction data prior to signing and verifying.
署名および検証の前に、ネットワーク生成ハッシュをトランザクションデータに付加することによって、トランザクションが異なるネットワークで再生されないようにします。
どうやら、トランザクションの署名をする際には、network generation hash
が必要になるみたいです。
network generation hash
は、おそらくネメシスブロックのジェネレーションハッシュのことであると推測されます。
ジェネレーションハッシュ
ジェネレーションハッシュは以下の資料にあるように、ブロックの生成に必要な値です。
導出方法とか、何に使われているのかは、よく調べていません。
ネメシスブロックのジェネレーションハッシュの探しかた
ネメシスブロックの情報をRESTに問い合わせます。generationHash
と書いてある箇所がそうです。
ネットワーク毎に異なるので、プライベートネットワークを使っている場合は注意しましょう。
ジェネレーションハッシュをどう使うか
この段階で分かっているのは、トランザクションに含めることだけです。
よくわかんないので、従来通りトランスファートランザクションをなげました。
2019-06-01 09:28:24.303162 0x00007f1ce47e8700: <warning> (subscribers::SubscriptionManager.cpp@101) rejected tx 7D1CDD44C55F847A67A876D2025521A82BFA138DE495EE184326AA8CE01E4E95 due to result Failure_Signature_Not_Verifiable (deadline 99919702895)
2019-06-01 09:28:24.303637 0x00007f1ce47e8700: <warning> (consumers::StatelessValidationConsumer.cpp@47) stateless transaction validation failed: Failure_Signature_Not_Verifiable
2019-06-01 09:28:24.326207 0x00007f1ce3fe7700: <warning> (consumers::ReclaimMemoryInspector.cpp@32) consumer aborted at position 0 while processing 1 txes [7D1CDD44] from Remote_Push due to Failure_Signature_Not_Verifiable
Failure_Signature_Not_Verifiable
と出ました。つまり、署名対象データが変わったということが推測されます。
後で分かったことなのですが、トランザクションハッシュの計算方法も変わっていました。
署名とハッシュの算出について参考にした箇所
いろいろ探し回って、それっぽいにおいがする箇所を見つけました。
ジェネレーションハッシュをどのように使うかは、次でまとめます。
署名対象データ
これまで
項目 | サイズ | 意味 |
---|---|---|
version | 2byte(LE) | 0390 |
type | 2byte(LE) | 5441 |
fee | 8byte(LE) | 手数料 |
deadline | 8byte(LE) | 期限 |
受取人 | 25byte | 受取人アドレス |
msg size | 2byte(LE) | メッセージバイト数+1 |
num mosaics | 1byte | モザイクの種類数 |
msg payload | 1byte + 任意byte | メッセージタイプ+メッセージ |
mosaics | array(8byte(LE), 8byte(LE)) | モザイクID+量 |
これから
先頭に追加です
項目 | サイズ | 意味 |
---|---|---|
nemesisGenerationHash | 32byte | ネメシスブロックのジェネレーションハッシュ |
あとは同じ |
ハッシュ計算データ
これまで
項目 | サイズ | 意味 |
---|---|---|
署名の先頭32byte | 32byte | 署名のR |
公開鍵 | 32byte | 署名者の公開鍵 |
version | 2byte(LE) | 0390 |
type | 2byte(LE) | 5441 |
fee | 8byte(LE) | 手数料 |
deadline | 8byte(LE) | 期限 |
受取人 | 25byte | 受取人アドレス |
msg size | 2byte(LE) | メッセージバイト数+1 |
num mosaics | 1byte | モザイクの種類数 |
msg payload | 1byte + 任意byte | メッセージタイプ+メッセージ |
mosaics | array(8byte(LE), 8byte(LE)) | モザイクID+量 |
これから
あいだに追加です
項目 | サイズ | 意味 |
---|---|---|
署名の先頭32byte | 32byte | 署名のR |
公開鍵 | 32byte | 署名者の公開鍵 |
nemesisGenerationHash | 32byte | ネメシスブロックのジェネレーションハッシュ |
version | 2byte(LE) | 0390 |
type | 2byte(LE) | 5441 |
fee | 8byte(LE) | 手数料 |
deadline | 8byte(LE) | 期限 |
受取人 | 25byte | 受取人アドレス |
msg size | 2byte(LE) | メッセージバイト数+1 |
num mosaics | 1byte | モザイクの種類数 |
msg payload | 1byte + 任意byte | メッセージタイプ+メッセージ |
mosaics | array(8byte(LE), 8byte(LE)) | モザイクID+量 |
トランザクション投げるためのかんたんなコード
process.env.PRIVATE_KEY = '25B3F54217340F7061D02676C4B928ADB4395EB70A2A52D2A11E2F4AE011B03E';
process.env.nemesisGenerationHash = '5ABBD9F7894EE7E5D4C3CDA934245396AEFCD1CB0426F265AC81F4F3450AB6DD';
process.env.NODE_URL = 'http://3.112.207.254:3000'
const nem2Sdk = require("nem2-sdk");
const jssha3 = require('js-sha3');
const nem2lib = require("nem2-library");
const request = require('request');
const sha3_256 = jssha3.sha3_256;
const Address = nem2Sdk.Address,
Deadline = nem2Sdk.Deadline,
Account = nem2Sdk.Account,
UInt64 = nem2Sdk.UInt64,
NetworkType = nem2Sdk.NetworkType,
PlainMessage = nem2Sdk.PlainMessage,
TransferTransaction = nem2Sdk.TransferTransaction,
Mosaic = nem2Sdk.Mosaic,
MosaicId = nem2Sdk.MosaicId,
NamespaceId = nem2Sdk.NamespaceId,
TransactionHttp = nem2Sdk.TransactionHttp,
NetworkCurrencyMosaic = nem2Sdk.NetworkCurrencyMosaic;
const recipientAddress = Address.createFromRawAddress('SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54');
const tx = TransferTransaction.create(
Deadline.create(),
recipientAddress,
[NetworkCurrencyMosaic.createRelative(0)],
PlainMessage.create(''),
NetworkType.MIJIN_TEST,
UInt64.fromUint(0)
)
const account = Account.createFromPrivateKey(process.env.PRIVATE_KEY, NetworkType.MIJIN_TEST);
const signedTransaction = account.sign(tx);
const txPayload = signedTransaction.payload
const txPayloadSigningBytes = process.env.nemesisGenerationHash + txPayload.substr(100*2);
const keypair = nem2lib.KeyPair.createKeyPairFromPrivateKeyString(process.env.PRIVATE_KEY);
const signatureByte = nem2lib.KeyPair.sign(keypair, txPayloadSigningBytes);
const signature = nem2lib.convert.uint8ToHex(signatureByte);
const signedTxPayload =
txPayload.substr(0,4*2) +
signature +
txPayload.substr((4+64)*2);
const hashInputPayload =
signedTxPayload.substr(4*2,32*2) +
signedTxPayload.substr((4+64)*2,32*2) +
process.env.nemesisGenerationHash +
signedTxPayload.substr((4+64+32)*2);
const signedTxHash = sha3_256.create().update(Buffer.from(hashInputPayload, 'hex')).hex().toUpperCase();
console.log(`signedTxHash: ${signedTxHash}`);
const ENDPOINT = process.env.NODE_URL;
request({
url: `${ENDPOINT}/transaction`,
method: 'PUT',
headers: {
'Content-Type':'application/json'
},
json: {"payload": signedTxPayload}
}, (error, response, body) => {
console.log(body);
});
なげた
> node .\transfer2.js
signedTxHash: 957DA42927A7790F9F9C161B98B630568DE2707562AE5F6D58154600CE918332
{ message: 'packet 9 was pushed to the network via /transaction' }
おわりに
リプレイプロテクションが実装されました。