mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Prevent replay attacks with NetworkID field: (#4370)
Add a `NetworkID` field to help prevent replay attacks on and from
side-chains.
The new field must be used when the server is using a network id > 1024.
To preserve legacy behavior, all chains with a network ID less than 1025
retain the existing behavior. This includes Mainnet, Testnet, Devnet,
and hooks-testnet. If `sfNetworkID` is present in any transaction
submitted to any of the nodes on one of these chains, then
`telNETWORK_ID_MAKES_TX_NON_CANONICAL` is returned.
Since chains with a network ID less than 1025, including Mainnet, retain
the existing behavior, there is no need for an amendment.
The `NetworkID` helps to prevent replay attacks because users specify a
`NetworkID` field in every transaction for that chain.
This change introduces a new UINT32 field, `sfNetworkID` ("NetworkID").
There are also three new local error codes for transaction results:
- `telNETWORK_ID_MAKES_TX_NON_CANONICAL`
- `telREQUIRES_NETWORK_ID`
- `telWRONG_NETWORK`
To learn about the other transaction result codes, see:
https://xrpl.org/transaction-results.html
Local error codes were chosen because a transaction is not necessarily
malformed if it is submitted to a node running on the incorrect chain.
This is a local error specific to that node and could be corrected by
switching to a different node or by changing the `network_id` on that
node. See:
https://xrpl.org/connect-your-rippled-to-the-xrp-test-net.html
In addition to using `NetworkID`, it is still generally recommended to
use different accounts and keys on side-chains. However, people will
undoubtedly use the same keys on multiple chains; for example, this is
common practice on other blockchain networks. There are also some
legitimate use cases for this.
A `app.NetworkID` test suite has been added, and `core.Config` was
updated to include some network_id tests.
This commit is contained in:
154
src/test/app/NetworkID_test.cpp
Normal file
154
src/test/app/NetworkID_test.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 Dev Null Productions
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/basics/BasicConfig.h>
|
||||
#include <ripple/core/ConfigSections.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class NetworkID_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testNetworkID();
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
makeNetworkConfig(uint32_t networkID)
|
||||
{
|
||||
using namespace jtx;
|
||||
return envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
cfg->NETWORK_ID = networkID;
|
||||
return cfg;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testNetworkID()
|
||||
{
|
||||
testcase(
|
||||
"Require txn NetworkID to be specified (or not) depending on the "
|
||||
"network ID of the node");
|
||||
using namespace jtx;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
|
||||
auto const runTx = [&](test::jtx::Env& env,
|
||||
Json::Value const& jv,
|
||||
TER expectedOutcome) {
|
||||
env.memoize(env.master);
|
||||
env.memoize(alice);
|
||||
|
||||
// fund alice
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = env.master.human();
|
||||
jv[jss::Destination] = alice.human();
|
||||
jv[jss::TransactionType] = "Payment";
|
||||
jv[jss::Amount] = "10000000000";
|
||||
if (env.app().config().NETWORK_ID > 1024)
|
||||
jv[jss::NetworkID] =
|
||||
std::to_string(env.app().config().NETWORK_ID);
|
||||
|
||||
env(jv, fee(1000), sig(env.master));
|
||||
}
|
||||
|
||||
// run tx
|
||||
env(jv, fee(1000), ter(expectedOutcome));
|
||||
env.close();
|
||||
};
|
||||
|
||||
// test mainnet
|
||||
{
|
||||
test::jtx::Env env{*this, makeNetworkConfig(0)};
|
||||
BEAST_EXPECT(env.app().config().NETWORK_ID == 0);
|
||||
|
||||
// try to submit a txn without network id, this should work
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[jss::TransactionType] = jss::AccountSet;
|
||||
runTx(env, jv, tesSUCCESS);
|
||||
|
||||
// try to submit a txn with NetworkID present against a mainnet
|
||||
// node, this will fail
|
||||
jv[jss::NetworkID] = 0;
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
|
||||
// change network id to something else, should still return same
|
||||
// error
|
||||
jv[jss::NetworkID] = 10000;
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
}
|
||||
|
||||
// any network up to and including networkid 1024 cannot support
|
||||
// NetworkID
|
||||
{
|
||||
test::jtx::Env env{*this, makeNetworkConfig(1024)};
|
||||
BEAST_EXPECT(env.app().config().NETWORK_ID == 1024);
|
||||
|
||||
// try to submit a txn without network id, this should work
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[jss::TransactionType] = jss::AccountSet;
|
||||
runTx(env, jv, tesSUCCESS);
|
||||
|
||||
// now submit with a network id, this will fail
|
||||
jv[jss::NetworkID] = 1024;
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
|
||||
jv[jss::NetworkID] = 1000;
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
}
|
||||
|
||||
// any network above networkid 1024 will produce an error if fed a txn
|
||||
// absent networkid
|
||||
{
|
||||
test::jtx::Env env{*this, makeNetworkConfig(1025)};
|
||||
BEAST_EXPECT(env.app().config().NETWORK_ID == 1025);
|
||||
|
||||
// try to submit a txn without network id, this should not work
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[jss::TransactionType] = jss::AccountSet;
|
||||
runTx(env, jv, telREQUIRES_NETWORK_ID);
|
||||
|
||||
// try to submit with wrong network id
|
||||
jv[jss::NetworkID] = 0;
|
||||
runTx(env, jv, telWRONG_NETWORK);
|
||||
|
||||
jv[jss::NetworkID] = 1024;
|
||||
runTx(env, jv, telWRONG_NETWORK);
|
||||
|
||||
// submit the correct network id
|
||||
jv[jss::NetworkID] = 1025;
|
||||
runTx(env, jv, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(NetworkID, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -411,6 +411,71 @@ port_wss_admin
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testNetworkID()
|
||||
{
|
||||
testcase("network id");
|
||||
std::string error;
|
||||
Config c;
|
||||
try
|
||||
{
|
||||
c.loadFromString(R"rippleConfig(
|
||||
[network_id]
|
||||
main
|
||||
)rippleConfig");
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(error == "");
|
||||
BEAST_EXPECT(c.NETWORK_ID == 0);
|
||||
|
||||
try
|
||||
{
|
||||
c.loadFromString(R"rippleConfig(
|
||||
)rippleConfig");
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(error == "");
|
||||
BEAST_EXPECT(c.NETWORK_ID == 0);
|
||||
|
||||
try
|
||||
{
|
||||
c.loadFromString(R"rippleConfig(
|
||||
[network_id]
|
||||
255
|
||||
)rippleConfig");
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(error == "");
|
||||
BEAST_EXPECT(c.NETWORK_ID == 255);
|
||||
|
||||
try
|
||||
{
|
||||
c.loadFromString(R"rippleConfig(
|
||||
[network_id]
|
||||
10000
|
||||
)rippleConfig");
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(error == "");
|
||||
BEAST_EXPECT(c.NETWORK_ID == 10000);
|
||||
}
|
||||
|
||||
void
|
||||
testValidatorsFile()
|
||||
{
|
||||
@@ -1230,6 +1295,7 @@ r.ripple.com:51235
|
||||
testGetters();
|
||||
testAmendment();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -163,7 +163,10 @@ Env::lookup(AccountID const& id) const
|
||||
{
|
||||
auto const iter = map_.find(id);
|
||||
if (iter == map_.end())
|
||||
{
|
||||
std::cout << "Unknown account: " << id << "\n";
|
||||
Throw<std::runtime_error>("Env::lookup:: unknown account ID");
|
||||
}
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user