diff --git a/programs/leap-util/CMakeLists.txt b/programs/leap-util/CMakeLists.txt index af28a0d930..0a00136ce9 100644 --- a/programs/leap-util/CMakeLists.txt +++ b/programs/leap-util/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable( ${LEAP_UTIL_EXECUTABLE_NAME} main.cpp actions/subcommand.cpp actions/generic.cpp actions/blocklog.cpp actions/snapshot.cpp actions/chain.cpp) +add_executable( ${LEAP_UTIL_EXECUTABLE_NAME} main.cpp actions/subcommand.cpp actions/generic.cpp actions/blocklog.cpp actions/bls.cpp actions/snapshot.cpp actions/chain.cpp) if( UNIX AND NOT APPLE ) set(rt_library rt ) diff --git a/programs/leap-util/actions/bls.cpp b/programs/leap-util/actions/bls.cpp new file mode 100644 index 0000000000..4ed94834b7 --- /dev/null +++ b/programs/leap-util/actions/bls.cpp @@ -0,0 +1,125 @@ +#include "bls.hpp" + +#include +#include +#include + +#include + +using namespace fc::crypto::blslib; +namespace bpo = boost::program_options; +using bpo::options_description; + +void bls_actions::setup(CLI::App& app) { + // callback helper with error code handling + auto err_guard = [this](int (bls_actions::*fun)()) { + try { + int rc = (this->*fun)(); + if(rc) throw(CLI::RuntimeError(rc)); + } catch(...) { + print_exception(); + throw(CLI::RuntimeError(-1)); + } + }; + + // main command + auto* sub = app.add_subcommand("bls", "BLS utility"); + sub->require_subcommand(); + + // Create subcommand + auto create = sub->add_subcommand("create", "Create BLS items"); + create->require_subcommand(); + + // sub-subcommand - key + auto* create_key = create->add_subcommand("key", "Create a new BLS keypair and print the public and private keys")->callback([err_guard]() { err_guard(&bls_actions::create_key); }); + create_key->add_option("-f,--file", opt->key_file, "Name of file to write private/public key output to. (Must be set, unless \"--to-console\" is passed"); + create_key->add_flag( "--to-console", opt->print_console, "Print private/public keys to console."); + + // sub-subcommand - pop (proof of possession) + auto* create_pop = create->add_subcommand("pop", "Create proof of possession of the corresponding private key for a given public key")->callback([err_guard]() { err_guard(&bls_actions::create_pop); }); + create_pop->add_option("-f,--file", opt->key_file, "Name of file storing the private key. (one and only one of \"-f,--file\" and \"--private-key\" must be set)"); + create_pop->add_option("--private-key", opt->private_key_str, "The private key. (one and only one of \"-f,--file\" and \"--private-key\" must be set)"); +} + +int bls_actions::create_key() { + if (opt->key_file.empty() && !opt->print_console) { + std::cerr << "ERROR: Either indicate a file using \"-f, --file\" or pass \"--to-console\"" << "\n"; + return -1; + } else if (!opt->key_file.empty() && opt->print_console) { + std::cerr << "ERROR: Only one of \"-f, --file\" or pass \"--to-console\" can be provided" << "\n"; + return -1; + } + + // create a private key and get its corresponding public key + const bls_private_key private_key = bls_private_key::generate(); + const bls_public_key public_key = private_key.get_public_key(); + + // generate pop + const std::string pop_str = generate_pop_str(private_key); + + // prepare output + std::string out_str = "Private key: " + private_key.to_string({}) + "\n"; + out_str += "Public key: " + public_key.to_string({}) + "\n"; + out_str += "Proof of Possession: " + pop_str + "\n"; + if (opt->print_console) { + std::cout << out_str; + } else { + std::cout << "saving keys to " << opt->key_file << "\n"; + std::ofstream out( opt->key_file.c_str() ); + out << out_str; + } + + return 0; +} + +int bls_actions::create_pop() { + if (opt->key_file.empty() && opt->private_key_str.empty()) { + std::cerr << "ERROR: Either indicate a file using \"-f, --file\" or pass \"--private-key\"" << "\n"; + return -1; + } else if (!opt->key_file.empty() && !opt->private_key_str.empty()) { + std::cerr << "ERROR: Only one of \"-f, --file\" and \"--private-key\" can be provided" << "\n"; + return -1; + } + + std::string private_key_str; + if (!opt->private_key_str.empty()) { + private_key_str = opt->private_key_str; + } else { + std::ifstream key_file(opt->key_file); + + if (!key_file.is_open()) { + std::cerr << "ERROR: failed to open file " << opt->key_file << "\n"; + return -1; + } + + if (std::getline(key_file, private_key_str)) { + if (!key_file.eof()) { + std::cerr << "ERROR: file " << opt->key_file << " contains more than one line" << "\n"; + return -1; + } + } else { + std::cerr << "ERROR: file " << opt->key_file << " is empty" << "\n"; + return -1; + } + } + + // create private key object using input private key string + const bls_private_key private_key = bls_private_key(private_key_str); + const bls_public_key public_key = private_key.get_public_key(); + std::string pop_str = generate_pop_str(private_key); + + std::cout << "Proof of Possession: " << pop_str << "\n"; + std::cout << "Public key: " << public_key.to_string({}) << "\n"; + + return 0; +} + +std::string bls_actions::generate_pop_str(const bls_private_key& private_key) { + const bls_public_key public_key = private_key.get_public_key(); + + const std::array msg = public_key._pkey.toAffineBytesLE(true); // true means raw + const std::vector msg_vector = std::vector(msg.begin(), msg.end()); + const bls_signature pop = private_key.sign(msg_vector); + + return pop.to_string({}); +} diff --git a/programs/leap-util/actions/bls.hpp b/programs/leap-util/actions/bls.hpp new file mode 100644 index 0000000000..aba3545e5e --- /dev/null +++ b/programs/leap-util/actions/bls.hpp @@ -0,0 +1,26 @@ +#include "subcommand.hpp" +#include + +#include + +using namespace eosio::chain; + +struct bls_options { + std::string key_file; + std::string private_key_str; + + // flags + bool print_console{false}; +}; + +class bls_actions : public sub_command { +public: + void setup(CLI::App& app); + +protected: + int create_key(); + int create_pop(); + +private: + std::string generate_pop_str(const fc::crypto::blslib::bls_private_key& private_key); +}; diff --git a/programs/leap-util/main.cpp b/programs/leap-util/main.cpp index 908a760cb3..840cfa5b22 100644 --- a/programs/leap-util/main.cpp +++ b/programs/leap-util/main.cpp @@ -6,6 +6,7 @@ #include #include "actions/blocklog.hpp" +#include "actions/bls.hpp" #include "actions/chain.hpp" #include "actions/generic.hpp" #include "actions/snapshot.hpp" @@ -33,6 +34,10 @@ int main(int argc, char** argv) { auto blocklog_subcommand = std::make_shared(); blocklog_subcommand->setup(app); + // bls sc tree + auto bls_subcommand = std::make_shared(); + bls_subcommand->setup(app); + // snapshot sc tree auto snapshot_subcommand = std::make_shared(); snapshot_subcommand->setup(app); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 71147aaa34..cfae3e4226 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/block_log_util_test.py ${CMAKE_CURREN configure_file(${CMAKE_CURRENT_SOURCE_DIR}/block_log_retain_blocks_test.py ${CMAKE_CURRENT_BINARY_DIR}/block_log_retain_blocks_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cluster_launcher.py ${CMAKE_CURRENT_BINARY_DIR}/cluster_launcher.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/distributed-transactions-test.py ${CMAKE_CURRENT_BINARY_DIR}/distributed-transactions-test.py COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/leap_util_bls_test.py ${CMAKE_CURRENT_BINARY_DIR}/leap_util_bls_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/sample-cluster-map.json ${CMAKE_CURRENT_BINARY_DIR}/sample-cluster-map.json COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/restart-scenarios-test.py ${CMAKE_CURRENT_BINARY_DIR}/restart-scenarios-test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/terminate-scenarios-test.py ${CMAKE_CURRENT_BINARY_DIR}/terminate-scenarios-test.py COPYONLY) @@ -241,6 +242,8 @@ set_property(TEST cli_test PROPERTY LABELS nonparallelizable_tests) add_test(NAME larger_lib_test COMMAND tests/large-lib-test.py ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST larger_lib_test PROPERTY LABELS nonparallelizable_tests) +add_test(NAME leap_util_bls_test COMMAND tests/leap_util_bls_test.py WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + add_test(NAME http_plugin_test COMMAND tests/http_plugin_test.py ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_tests_properties(http_plugin_test PROPERTIES TIMEOUT 100) set_property(TEST http_plugin_test PROPERTY LABELS nonparallelizable_tests) diff --git a/tests/leap_util_bls_test.py b/tests/leap_util_bls_test.py new file mode 100755 index 0000000000..e4a2e28f99 --- /dev/null +++ b/tests/leap_util_bls_test.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 + +import os +import re + +from TestHarness import Utils + +############################################################### +# leap_util_bls_test +# +# Test leap-util's BLS commands. +# - Create a key pair +# - Create a POP (Proof of Possession) +# - Error handlings +# +############################################################### + +Print=Utils.Print +testSuccessful=False + +def test_create_key_to_console(): + rslts = Utils.processLeapUtilCmd("bls create key --to-console", "create key to console", silentErrors=False) + check_create_key_results(rslts) + +def test_create_key_to_file(): + tmp_file = "tmp_key_file_dlkdx1x56pjy" + Utils.processLeapUtilCmd("bls create key --file {}".format(tmp_file), "create key to file", silentErrors=False) + + with open(tmp_file, 'r') as file: + rslts = file.read() + check_create_key_results(rslts) + + os.remove(tmp_file) + +def test_create_pop_from_command_line(): + # Create a pair of keys + rslts = Utils.processLeapUtilCmd("bls create key --to-console", "create key to console", silentErrors=False) + results = get_results(rslts) + + # save results + private_key = results["Private key"] + public_key = results["Public key"] + pop = results["Proof of Possession"] + + # use the private key to create POP + rslts = Utils.processLeapUtilCmd("bls create pop --private-key {}".format(private_key), "create pop from command line", silentErrors=False) + results = get_results(rslts) + + # check pop and public key are the same as those generated before + assert results["Public key"] == public_key + assert results["Proof of Possession"] == pop + +def test_create_pop_from_file(): + # Create a pair of keys + rslts = Utils.processLeapUtilCmd("bls create key --to-console", "create key to console", silentErrors=False) + results = get_results(rslts) + + # save results + private_key = results["Private key"] + public_key = results["Public key"] + pop = results["Proof of Possession"] + + # save private key to a file + private_key_file = "tmp_key_file_dlkdx1x56pjy" + with open(private_key_file, 'w') as file: + file.write(private_key) + + # use the private key file to create POP + rslts = Utils.processLeapUtilCmd("bls create pop --file {}".format(private_key_file), "create pop from command line", silentErrors=False) + os.remove(private_key_file) + results = get_results(rslts) + + # check pop and public key are the same as those generated before + assert results["Public key"] == public_key + assert results["Proof of Possession"] == pop + +def test_create_key_error_handling(): + # should fail with missing arguments (processLeapUtilCmd returning None) + assert Utils.processLeapUtilCmd("bls create key", "missing arguments") == None + + # should fail when both arguments are present + assert Utils.processLeapUtilCmd("bls create key --file out_file --to-console", "conflicting arguments") == None + +def test_create_pop_error_handling(): + # should fail with missing arguments (processLeapUtilCmd returning None) + assert Utils.processLeapUtilCmd("bls create pop", "missing arguments") == None + + # should fail when both arguments are present + assert Utils.processLeapUtilCmd("bls create pop --file private_key_file --private-key", "conflicting arguments") == None + + # should fail when private key file does not exist + temp_file = "aRandomFileT6bej2pjsaz" + if os.path.exists(temp_file): + os.remove(temp_file) + assert Utils.processLeapUtilCmd("bls create pop --file {}".format(temp_file), "private file not existing") == None + +def check_create_key_results(rslts): + results = get_results(rslts) + + # check each output has valid value + assert "PVT_BLS_" in results["Private key"] + assert "PUB_BLS_" in results["Public key"] + assert "SIG_BLS_" in results["Proof of Possession"] + +def get_results(rslts): + # sample output looks like + # Private key: PVT_BLS_kRhJJ2MsM+/CddO... + # Public key: PUB_BLS_lbUE8922wUfX0Iy5... + # Proof of Possession: SIG_BLS_olZfcFw... + pattern = r'(\w+[^:]*): ([^\n]+)' + matched= re.findall(pattern, rslts) + + results = {} + for k, v in matched: + results[k.strip()] = v.strip() + + return results + +# tests start +try: + # test create key to console + test_create_key_to_console() + + # test create key to file + test_create_key_to_file() + + # test create pop from private key in command line + test_create_pop_from_command_line() + + # test create pop from private key in file + test_create_pop_from_file() + + # test error handling in create key + test_create_key_error_handling() + + # test error handling in create pop + test_create_pop_error_handling() + + testSuccessful=True +except Exception as e: + Print(e) + Utils.errorExit("exception during processing") + +exitCode = 0 if testSuccessful else 1 +exit(exitCode)