From 29425095e04d71ab97874b8d5cb9f7fb3649e609 Mon Sep 17 00:00:00 2001 From: Ravin Perera <33562092+ravinsp@users.noreply.github.com> Date: Wed, 25 Nov 2020 07:10:07 +0530 Subject: [PATCH] User nonce validation and expiration. (#164) --- CMakeLists.txt | 1 + examples/nodejs_client/hp-client-lib.js | 2 + src/consensus.cpp | 9 +--- src/msg/usrmsg_common.hpp | 2 +- src/usr/input_nonce_map.cpp | 59 +++++++++++++++++++++++++ src/usr/input_nonce_map.hpp | 21 +++++++++ src/usr/usr.cpp | 47 ++++++++++++-------- src/usr/usr.hpp | 3 +- 8 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 src/usr/input_nonce_map.cpp create mode 100644 src/usr/input_nonce_map.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3be0a943..69fa110d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ add_executable(hpcore src/p2p/p2p.cpp src/usr/user_comm_session.cpp src/usr/user_session_handler.cpp + src/usr/input_nonce_map.cpp src/usr/usr.cpp src/usr/read_req.cpp src/ledger.cpp diff --git a/examples/nodejs_client/hp-client-lib.js b/examples/nodejs_client/hp-client-lib.js index a90dc1f0..af975b7a 100644 --- a/examples/nodejs_client/hp-client-lib.js +++ b/examples/nodejs_client/hp-client-lib.js @@ -185,6 +185,8 @@ function HotPocketClient(server, keys, protocol = protocols.BSON) { if (!nonce) nonce = (new Date()).getTime().toString(); + else + nonce = nonce.toString(); // Acquire the current lcl and add the specified offset. const stat = await this.getStatus(); diff --git a/src/consensus.cpp b/src/consensus.cpp index 4714307c..c5f91f54 100644 --- a/src/consensus.cpp +++ b/src/consensus.cpp @@ -421,7 +421,6 @@ namespace consensus // We only process inputs in the submitted order that can be satisfied with the remaining account balance. size_t total_input_len = 0; bool appbill_balance_exceeded = false; - util::rollover_hashset recent_user_input_hashes(200); for (const usr::user_input &umsg : umsgs) { @@ -436,13 +435,9 @@ namespace consensus util::buffer_view input; std::string hash; uint64_t max_lcl_seqno; - reject_reason = usr::validate_user_input_submission(pubkey, umsg, lcl_seq_no, total_input_len, recent_user_input_hashes, - hash, input, max_lcl_seqno); + reject_reason = usr::validate_user_input_submission(pubkey, umsg, lcl_seq_no, total_input_len, hash, input, max_lcl_seqno); - if (input.is_null()) - return -1; - - if (reject_reason == NULL) + if (reject_reason == NULL && !input.is_null()) { // No reject reason means we should go ahead and subject the input to consensus. ctx.candidate_user_inputs.try_emplace( diff --git a/src/msg/usrmsg_common.hpp b/src/msg/usrmsg_common.hpp index 567896c6..d1adc8c9 100644 --- a/src/msg/usrmsg_common.hpp +++ b/src/msg/usrmsg_common.hpp @@ -42,10 +42,10 @@ namespace msg::usrmsg constexpr const char *STATUS_REJECTED = "rejected"; constexpr const char *REASON_BAD_MSG_FORMAT = "bad_msg_format"; constexpr const char *REASON_INVALID_MSG_TYPE = "invalid_msg_type"; - constexpr const char *REASON_DUPLICATE_MSG = "dup_msg"; constexpr const char *REASON_BAD_SIG = "bad_sig"; constexpr const char *REASON_APPBILL_BALANCE_EXCEEDED = "appbill_balance_exceeded"; constexpr const char *REASON_MAX_LEDGER_EXPIRED = "max_ledger_expired"; + constexpr const char *REASON_NONCE_EXPIRED = "nonce_expired"; } // namespace msg::usrmsg diff --git a/src/usr/input_nonce_map.cpp b/src/usr/input_nonce_map.cpp new file mode 100644 index 00000000..5fa20bd0 --- /dev/null +++ b/src/usr/input_nonce_map.cpp @@ -0,0 +1,59 @@ +#include "../pchheader.hpp" +#include "../util/util.hpp" +#include "input_nonce_map.hpp" + +namespace usr +{ + constexpr uint64_t TTL = 300000; // 5 minutes. + constexpr uint16_t CLEANUP_THRESHOLD = 256; + + /** + * Checks whether the given nonce is valid for the given user pubkey. If it is valid, remembers this nonce + * to be checked for future checks. (If no_add is true, this nonce will not be remembered) + */ + bool input_nonce_map::is_valid(const std::string &pubkey, const std::string &nonce, const bool no_add) + { + bool valid = false; + + const uint64_t now = util::get_epoch_milliseconds(); + auto itr = nonce_map.find(pubkey); + if (itr == nonce_map.end()) + { + valid = true; + if (!no_add) + nonce_map.emplace(pubkey, std::pair(nonce, util::get_epoch_milliseconds() + TTL)); + } + else + { + const std::string &existing_nonce = itr->second.first; + const uint64_t expire_on = itr->second.second; + valid = (expire_on <= now || existing_nonce < nonce); + + if (valid && !no_add) + { + itr->second.first = nonce; + itr->second.second = now + TTL; + } + } + + if (nonce_map.size() > CLEANUP_THRESHOLD) + cleanup(); + + return valid; + } + + void input_nonce_map::cleanup() + { + const uint64_t now = util::get_epoch_milliseconds(); + + for (auto itr = nonce_map.begin(); itr != nonce_map.end();) + { + const uint64_t expire_on = itr->second.second; + if (expire_on <= now) + itr = nonce_map.erase(itr); + else + itr++; + } + } + +} // namespace usr diff --git a/src/usr/input_nonce_map.hpp b/src/usr/input_nonce_map.hpp new file mode 100644 index 00000000..6f31ff95 --- /dev/null +++ b/src/usr/input_nonce_map.hpp @@ -0,0 +1,21 @@ +#ifndef _HP_USR_INPUT_NONCE_MAP_ +#define _HP_USR_INPUT_NONCE_MAP_ + +#include "../pchheader.hpp" + +namespace usr +{ + class input_nonce_map + { + private: + // Keeps short-lived items with their absolute expiration time. + std::unordered_map> nonce_map; + void cleanup(); + + public: + bool is_valid(const std::string &pubkey, const std::string &nonce, const bool no_add = false); + }; + +} // namespace usr + +#endif \ No newline at end of file diff --git a/src/usr/usr.cpp b/src/usr/usr.cpp index e135fe9a..04014535 100644 --- a/src/usr/usr.cpp +++ b/src/usr/usr.cpp @@ -14,6 +14,7 @@ #include "user_comm_server.hpp" #include "user_input.hpp" #include "read_req.hpp" +#include "input_nonce_map.hpp" namespace usr { @@ -22,6 +23,7 @@ namespace usr connected_context ctx; util::buffer_store input_store; + input_nonce_map nonce_map; uint64_t metric_thresholds[5]; bool init_success = false; @@ -144,12 +146,25 @@ namespace usr { std::scoped_lock lock(ctx.users_mutex); - //Add to the submitted input list. - user.submitted_inputs.push_back(user_input( - std::move(input_container), - std::move(sig), - user.protocol)); - return 0; + std::string input_data; + std::string nonce; + uint64_t max_lcl_seqno; + parser.extract_input_container(input_data, nonce, max_lcl_seqno, input_container); + + if (nonce_map.is_valid(user.pubkey, nonce, true)) + { + //Add to the submitted input list. + user.submitted_inputs.push_back(user_input( + std::move(input_container), + std::move(sig), + user.protocol)); + return 0; + } + else + { + send_input_status(parser, user.session, msg::usrmsg::STATUS_REJECTED, msg::usrmsg::REASON_NONCE_EXPIRED, sig); + return -1; + } } else { @@ -262,20 +277,10 @@ namespace usr * Validates the provided user input message against all the required criteria. * @return The rejection reason if input rejected. NULL if the input can be accepted. */ - const char *validate_user_input_submission(const std::string_view user_pubkey, const usr::user_input &umsg, + const char *validate_user_input_submission(const std::string &user_pubkey, const usr::user_input &umsg, const uint64_t lcl_seq_no, size_t &total_input_len, - util::rollover_hashset &recent_user_input_hashes, std::string &hash, util::buffer_view &input, uint64_t &max_lcl_seqno) { - const std::string sig_hash = crypto::get_hash(umsg.sig); - - // Check for duplicate messages using hash of the signature. - if (!recent_user_input_hashes.try_emplace(sig_hash)) - { - LOG_DEBUG << "Duplicate user message."; - return msg::usrmsg::REASON_DUPLICATE_MSG; - } - // Verify the signature of the input_container. if (crypto::verify(umsg.input_container, umsg.sig, user_pubkey) == -1) { @@ -296,6 +301,12 @@ namespace usr return msg::usrmsg::REASON_MAX_LEDGER_EXPIRED; } + if (!nonce_map.is_valid(user_pubkey, nonce)) + { + LOG_DEBUG << "User message nonce expired."; + return msg::usrmsg::REASON_NONCE_EXPIRED; + } + // Keep checking the subtotal of inputs extracted so far with the appbill account balance. total_input_len += input_data.length(); if (!verify_appbill_check(user_pubkey, total_input_len)) @@ -307,7 +318,7 @@ namespace usr // Hash is prefixed with the nonce to support user-defined sort order. hash = std::move(nonce); // Append the hash of the message signature to get the final hash. - hash.append(sig_hash); + hash.append(crypto::get_hash(umsg.sig)); // Copy the input data into the input store. std::string_view s(); diff --git a/src/usr/usr.hpp b/src/usr/usr.hpp index 47b535c0..4abf5325 100644 --- a/src/usr/usr.hpp +++ b/src/usr/usr.hpp @@ -79,9 +79,8 @@ namespace usr int remove_user(const std::string &pubkey); - const char *validate_user_input_submission(const std::string_view user_pubkey, const usr::user_input &umsg, + const char *validate_user_input_submission(const std::string &user_pubkey, const usr::user_input &umsg, const uint64_t lcl_seq_no, size_t &total_input_len, - util::rollover_hashset &recent_user_input_hashes, std::string &hash, util::buffer_view &input, uint64_t &max_lcl_seqno); bool verify_appbill_check(std::string_view pubkey, const size_t input_len);