Add host-side rns-provision tool and portable build infrastructure
- Add tools/keygen/rns-provision CLI - Generate Reticulum identities with TSV/JSON output - Add provisioning bundle support (--outdir, --bundle) - Write manifest.json + per-device identity.json/bin + label.txt - Enforce 0600 permissions on private key material - Add -h/--help and version output - Make build portable across distros (msgpack shim targets) - Integrate ArduinoJson include paths - Disable ReticulumShared build (static-only)
This commit is contained in:
parent
18e8d2c8ea
commit
84d947a3f0
2 changed files with 311 additions and 32 deletions
|
|
@ -1,39 +1,194 @@
|
|||
// keygen - generate microReticulum identity keypairs
|
||||
// rns-provision - generate microReticulum identity keypairs + provisioning bundles
|
||||
//
|
||||
// Example:
|
||||
// ./keygen --quantity 6 --format tsv
|
||||
// ./keygen -q 6 -f json
|
||||
// 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 <Identity.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
static void usage(const char* argv0) {
|
||||
#if __has_include(<filesystem>)
|
||||
#include <filesystem>
|
||||
namespace fs = std::filesystem;
|
||||
#else
|
||||
#error "This tool requires <filesystem> (C++17)."
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/stat.h>
|
||||
#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: " << argv0 << " --quantity N [--format tsv|json] [--public]\n"
|
||||
<< " -q, --quantity Number of identities to generate (required)\n"
|
||||
<< " -f, --format Output format: tsv (default) or json\n"
|
||||
<< " --public Also include public_key in output\n";
|
||||
<< "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<uint8_t>& 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<const char*>(bytes.data()), static_cast<std::streamsize>(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<uint8_t>.
|
||||
// This assumes the returned object supports .size() and operator[].
|
||||
// If your microReticulum Bytes type differs, we’ll adjust here.
|
||||
template <typename BytesT>
|
||||
static std::vector<uint8_t> to_u8vec(const BytesT& b) {
|
||||
std::vector<uint8_t> v;
|
||||
v.reserve(static_cast<size_t>(b.size()));
|
||||
for (size_t i = 0; i < static_cast<size_t>(b.size()); i++) {
|
||||
v.push_back(static_cast<uint8_t>(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]);
|
||||
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");
|
||||
|
|
@ -43,54 +198,176 @@ int main(int argc, char** argv) {
|
|||
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]);
|
||||
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<uint8_t> priv_bin;
|
||||
fs::path device_dir;
|
||||
};
|
||||
|
||||
std::vector<DeviceRec> devices;
|
||||
devices.reserve(static_cast<size_t>(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") {
|
||||
// header row
|
||||
std::cout << "n\tid_hex\tprivate_key_hex";
|
||||
std::cout << "n\tdevice\tid_hex\tprivate_key_hex";
|
||||
if (include_public) std::cout << "\tpublic_key_hex";
|
||||
std::cout << "\n";
|
||||
|
||||
for (int n = 1; n <= quantity; n++) {
|
||||
RNS::Identity id(true);
|
||||
std::cout
|
||||
<< n << "\t"
|
||||
<< id.hash().toHex() << "\t"
|
||||
<< id.get_private_key().toHex();
|
||||
if (include_public) std::cout << "\t" << id.get_public_key().toHex();
|
||||
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 {
|
||||
// json
|
||||
std::cout << "[\n";
|
||||
for (int n = 1; n <= quantity; n++) {
|
||||
RNS::Identity id(true);
|
||||
|
||||
for (size_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
std::cout << " {\n";
|
||||
std::cout << " \"n\": " << n << ",\n";
|
||||
std::cout << " \"id\": \"" << id.hash().toHex() << "\",\n";
|
||||
std::cout << " \"private_key\": \"" << id.get_private_key().toHex() << "\"";
|
||||
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\": \"" << id.get_public_key().toHex() << "\"\n";
|
||||
std::cout << ",\n \"public_key\": \"" << d.pub_hex << "\"\n";
|
||||
} else {
|
||||
std::cout << "\n";
|
||||
}
|
||||
std::cout << " }" << (n == quantity ? "\n" : ",\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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue