// rns-provision - generate microReticulum identity keypairs + provisioning bundles // // Examples: // ./rns-provision --quantity 6 --format tsv // ./rns-provision -q 6 -f json --public // ./rns-provision -q 6 --outdir provisioning/20260212_1030 --bundle both --public // // Notes: // - Writes sensitive material (private keys). Protect your output directories. // - identity.bin is raw private key bytes as returned by microReticulum Identity. // // $Header$ // $Id$ #include #include #include #include #include #include #include #include #include #include #include #include #include #if __has_include() #include namespace fs = std::filesystem; #else #error "This tool requires (C++17)." #endif #ifndef _WIN32 #include #endif static constexpr const char* TOOL_NAME = "rns-provision"; static constexpr const char* TOOL_VERSION = "0.1.0"; static void usage(const char* argv0, bool full = true) { std::cerr << TOOL_NAME << " " << TOOL_VERSION << "\n\n"; std::cerr << "Usage:\n" << " " << argv0 << " --quantity N [options]\n\n"; if (!full) return; std::cerr << "Options:\n" << " -q, --quantity N Number of identities to generate (required)\n" << " -f, --format FMT Stdout format: tsv (default) or json\n" << " --public Include public_key in stdout and identity.json\n" << " --outdir DIR Write provisioning bundle to DIR\n" << " --bundle MODE none|json|bin|both (default: both if --outdir)\n" << " --prefix NAME Device directory prefix (default: device)\n" << " -h, --help Show this help message and exit\n" << "\n" << "Examples:\n" << " " << argv0 << " -q 6\n" << " " << argv0 << " -q 6 --outdir provisioning/20260212_1030 --bundle both\n" << "\n"; } static bool is_flag(const std::string& a, const char* s) { return a == s; } static std::string utc_now_iso8601() { using namespace std::chrono; auto now = system_clock::now(); std::time_t t = system_clock::to_time_t(now); std::tm tm{}; #ifdef _WIN32 gmtime_s(&tm, &t); #else gmtime_r(&t, &tm); #endif std::ostringstream oss; oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ"); return oss.str(); } static std::string json_escape(const std::string& s) { std::ostringstream o; for (unsigned char c : s) { switch (c) { case '\"': o << "\\\""; break; case '\\': o << "\\\\"; break; case '\b': o << "\\b"; break; case '\f': o << "\\f"; break; case '\n': o << "\\n"; break; case '\r': o << "\\r"; break; case '\t': o << "\\t"; break; default: if (c < 0x20) { o << "\\u" << std::hex << std::setw(4) << std::setfill('0') << int(c); } else { o << c; } } } return o.str(); } static std::string zpad_int(int value, int width) { std::ostringstream oss; oss << std::setw(width) << std::setfill('0') << value; return oss.str(); } static void ensure_dir(const fs::path& p) { std::error_code ec; if (fs::exists(p, ec)) { if (!fs::is_directory(p, ec)) { throw std::runtime_error("Path exists but is not a directory: " + p.string()); } return; } if (!fs::create_directories(p, ec)) { throw std::runtime_error("Failed to create directory: " + p.string() + " (" + ec.message() + ")"); } } static void write_text_file(const fs::path& p, const std::string& data) { std::ofstream out(p, std::ios::out | std::ios::trunc); if (!out) throw std::runtime_error("Failed to open for write: " + p.string()); out << data; out.close(); if (!out) throw std::runtime_error("Failed writing file: " + p.string()); #ifndef _WIN32 // Best-effort: 0600 for sensitive text files (private keys) ::chmod(p.c_str(), 0600); #endif } static void write_binary_file(const fs::path& p, const std::vector& bytes) { std::ofstream out(p, std::ios::out | std::ios::binary | std::ios::trunc); if (!out) throw std::runtime_error("Failed to open for write: " + p.string()); out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); out.close(); if (!out) throw std::runtime_error("Failed writing file: " + p.string()); #ifndef _WIN32 ::chmod(p.c_str(), 0600); #endif } enum class BundleMode { NONE, JSON, BIN, BOTH }; static BundleMode parse_bundle_mode(const std::string& s) { if (s == "none") return BundleMode::NONE; if (s == "json") return BundleMode::JSON; if (s == "bin") return BundleMode::BIN; if (s == "both") return BundleMode::BOTH; throw std::runtime_error("Invalid --bundle value (must be none|json|bin|both)"); } // Convert microReticulum Bytes-ish type to std::vector. // This assumes the returned object supports .size() and operator[]. // If your microReticulum Bytes type differs, we’ll adjust here. template static std::vector to_u8vec(const BytesT& b) { std::vector v; v.reserve(static_cast(b.size())); for (size_t i = 0; i < static_cast(b.size()); i++) { v.push_back(static_cast(b[i])); } return v; } int main(int argc, char** argv) { try { int quantity = -1; std::string format = "tsv"; bool include_public = false; bool do_outdir = false; fs::path outdir; std::string prefix = "device"; BundleMode bundle_mode = BundleMode::NONE; bool bundle_mode_explicit = false; for (int i = 1; i < argc; i++) { std::string a(argv[i]); if (is_flag(a, "-h") || is_flag(a, "--help")) { usage(argv[0], true); return 0; } else if (is_flag(a, "-q") || is_flag(a, "--quantity")) { if (i + 1 >= argc) throw std::runtime_error("Missing value for --quantity"); quantity = std::stoi(argv[++i]); } else if (is_flag(a, "-f") || is_flag(a, "--format")) { if (i + 1 >= argc) throw std::runtime_error("Missing value for --format"); format = argv[++i]; } else if (is_flag(a, "--public")) { include_public = true; } else if (is_flag(a, "--outdir")) { if (i + 1 >= argc) throw std::runtime_error("Missing value for --outdir"); outdir = fs::path(argv[++i]); do_outdir = true; } else if (is_flag(a, "--bundle")) { if (i + 1 >= argc) throw std::runtime_error("Missing value for --bundle"); bundle_mode = parse_bundle_mode(argv[++i]); bundle_mode_explicit = true; } else if (is_flag(a, "--prefix")) { if (i + 1 >= argc) throw std::runtime_error("Missing value for --prefix"); prefix = argv[++i]; } else if (is_flag(a, "--version")) { std::cout << TOOL_NAME << " " << TOOL_VERSION << "\n"; return 0; } else { throw std::runtime_error("Unknown argument: " + a); } } if (quantity <= 0) { usage(argv[0], true); return 2; } if (!(format == "tsv" || format == "json")) { throw std::runtime_error("Invalid --format (must be tsv or json)"); } // Default bundle behavior: if user set --outdir but not --bundle, write both. if (do_outdir && !bundle_mode_explicit) { bundle_mode = BundleMode::BOTH; } if (do_outdir) { ensure_dir(outdir); } struct DeviceRec { int n; std::string device_name; std::string id_hex; std::string priv_hex; std::string pub_hex; std::vector priv_bin; fs::path device_dir; }; std::vector devices; devices.reserve(static_cast(quantity)); for (int n = 1; n <= quantity; n++) { RNS::Identity ident(true); DeviceRec rec; rec.n = n; rec.device_name = prefix + "_" + zpad_int(n, 3); // These methods matched your earlier build. If microReticulum changes, we adjust here. rec.id_hex = ident.hash().toHex(); rec.priv_hex = ident.get_private_key().toHex(); if (include_public) rec.pub_hex = ident.get_public_key().toHex(); // Write binary blob for the private key rec.priv_bin = to_u8vec(ident.get_private_key()); if (do_outdir) { rec.device_dir = outdir / rec.device_name; ensure_dir(rec.device_dir); } devices.push_back(std::move(rec)); } // ---- stdout ---- if (format == "tsv") { std::cout << "n\tdevice\tid_hex\tprivate_key_hex"; if (include_public) std::cout << "\tpublic_key_hex"; std::cout << "\n"; for (const auto& d : devices) { std::cout << d.n << "\t" << d.device_name << "\t" << d.id_hex << "\t" << d.priv_hex; if (include_public) std::cout << "\t" << d.pub_hex; std::cout << "\n"; } } else { std::cout << "[\n"; for (size_t i = 0; i < devices.size(); i++) { const auto& d = devices[i]; std::cout << " {\n"; std::cout << " \"n\": " << d.n << ",\n"; std::cout << " \"device\": \"" << json_escape(d.device_name) << "\",\n"; std::cout << " \"id\": \"" << d.id_hex << "\",\n"; std::cout << " \"private_key\": \"" << d.priv_hex << "\""; if (include_public) { std::cout << ",\n \"public_key\": \"" << d.pub_hex << "\"\n"; } else { std::cout << "\n"; } std::cout << " }" << (i + 1 == devices.size() ? "\n" : ",\n"); } std::cout << "]\n"; } // ---- bundle output ---- if (do_outdir && bundle_mode != BundleMode::NONE) { const std::string created_utc = utc_now_iso8601(); // manifest.json { std::ostringstream m; m << "{\n"; m << " \"schema_version\": 1,\n"; m << " \"created_utc\": \"" << created_utc << "\",\n"; m << " \"quantity\": " << quantity << ",\n"; m << " \"prefix\": \"" << json_escape(prefix) << "\",\n"; m << " \"devices\": [\n"; for (size_t i = 0; i < devices.size(); i++) { const auto& d = devices[i]; m << " {\n"; m << " \"n\": " << d.n << ",\n"; m << " \"device\": \"" << json_escape(d.device_name) << "\",\n"; m << " \"id\": \"" << d.id_hex << "\""; if (bundle_mode == BundleMode::JSON || bundle_mode == BundleMode::BOTH) { m << ",\n \"identity_json\": \"" << json_escape((d.device_name + "/identity.json")) << "\""; } if (bundle_mode == BundleMode::BIN || bundle_mode == BundleMode::BOTH) { m << ",\n \"identity_bin\": \"" << json_escape((d.device_name + "/identity.bin")) << "\""; } m << "\n }" << (i + 1 == devices.size() ? "\n" : ",\n"); } m << " ]\n"; m << "}\n"; write_text_file(outdir / "manifest.json", m.str()); } for (const auto& d : devices) { // label.txt { std::ostringstream l; l << d.device_name << "\n"; l << "id: " << d.id_hex << "\n"; write_text_file(d.device_dir / "label.txt", l.str()); } // identity.json if (bundle_mode == BundleMode::JSON || bundle_mode == BundleMode::BOTH) { std::ostringstream j; j << "{\n"; j << " \"schema_version\": 1,\n"; j << " \"created_utc\": \"" << created_utc << "\",\n"; j << " \"device\": \"" << json_escape(d.device_name) << "\",\n"; j << " \"id\": \"" << d.id_hex << "\",\n"; j << " \"private_key\": \"" << d.priv_hex << "\""; if (include_public) { j << ",\n \"public_key\": \"" << d.pub_hex << "\"\n"; } else { j << "\n"; } j << "}\n"; write_text_file(d.device_dir / "identity.json", j.str()); } // identity.bin if (bundle_mode == BundleMode::BIN || bundle_mode == BundleMode::BOTH) { write_binary_file(d.device_dir / "identity.bin", d.priv_bin); } } std::cerr << "Wrote provisioning bundle to: " << outdir.string() << "\n"; } return 0; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; return 1; } }