diff --git a/.gitignore b/.gitignore index 0c1cf50..dc427bb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ /toolchain/sysroot/ /toolchain/build/ -.vscode/ \ No newline at end of file +.vscode/ +check/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index b53ecc2..fd8c399 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set( HEADERS src/Services/DeviceControlService.h src/Services/EnrollmentService.h src/Security/TlsKeyUtil.h + src/Security/SslCertUtil.h ) add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} ) diff --git a/src/Security/SslCertUtil.h b/src/Security/SslCertUtil.h new file mode 100644 index 0000000..8d66d7f --- /dev/null +++ b/src/Security/SslCertUtil.h @@ -0,0 +1,512 @@ +// src/Security/SslCertUtil.h +#pragma once + +#include +#include +#include +#include +#include + +#include // chmod + +#include +#include +#include +#include +#include +#include + +#include + +namespace snoop +{ + namespace device_sec + { + + class SslCertUtil + { + public: + // Generates: + // - EC P-256 private key at keyPath (PEM, chmod 600) + // - CSR at csrPath with: + // CN = guid + // subjectAltName = URI:urn:device: + static void GenerateEcKeyAndCsr(const std::string &guid, + const std::filesystem::path &keyPath, + const std::filesystem::path &csrPath) + { + spdlog::info("SslCertUtil: generating EC key + CSR for GUID={}", guid); + + EVP_PKEY *pkey = nullptr; + EC_KEY *ec = nullptr; + + try + { + // ----- 1) Generate EC key (prime256v1) ----- + ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if (!ec) + { + throw std::runtime_error("EC_KEY_new_by_curve_name failed"); + } + if (EC_KEY_generate_key(ec) != 1) + { + throw std::runtime_error("EC_KEY_generate_key failed"); + } + + pkey = EVP_PKEY_new(); + if (!pkey) + { + throw std::runtime_error("EVP_PKEY_new failed"); + } + if (EVP_PKEY_assign_EC_KEY(pkey, ec) != 1) + { + throw std::runtime_error("EVP_PKEY_assign_EC_KEY failed"); + } + // pkey now owns ec + ec = nullptr; // prevent double free + + // ----- 5) Write key + CSR to disk ----- + SavePrivateKeyPem(pkey, keyPath); + BuildAndSaveCsr(pkey, guid, csrPath); + + spdlog::info("SslCertUtil: key written to '{}', csr written to '{}'", + keyPath.string(), csrPath.string()); + } + catch (...) + { + if (pkey) + { + EVP_PKEY_free(pkey); + } + if (ec) + { + EC_KEY_free(ec); + } + throw; // rethrow + } + + EVP_PKEY_free(pkey); + } + + // CSR from an existing key + static void GenerateCsrFromExistingKey(const std::string &guid, + const std::filesystem::path &keyPath, + const std::filesystem::path &csrPath) + { + spdlog::info("SslCertUtil: generating CSR from existing key '{}'", keyPath.string()); + + FILE *kf = fopen(keyPath.string().c_str(), "rb"); + if (!kf) + { + throw std::runtime_error("GenerateCsrFromExistingKey: cannot open key file: " + + keyPath.string()); + } + + EVP_PKEY *pkey = PEM_read_PrivateKey(kf, nullptr, nullptr, nullptr); + fclose(kf); + + if (!pkey) + { + throw std::runtime_error("GenerateCsrFromExistingKey: PEM_read_PrivateKey failed"); + } + + try + { + BuildAndSaveCsr(pkey, guid, csrPath); + } + catch (...) + { + EVP_PKEY_free(pkey); + throw; + } + + EVP_PKEY_free(pkey); + spdlog::info("SslCertUtil: CSR written to '{}'", csrPath.string()); + } + + // Encrypt file with AES-256-CBC + PBKDF2 + salt. + // + // Equivalent to: + // openssl enc -aes-256-cbc -pbkdf2 -salt -pass pass: -in in -out out + // + // NOTE: Does NOT shred or delete the input file; caller should do that + // (you’ll still call `shred -u` from EnrollmentService). + static void EncryptFileAes256CbcPbkdf2(const std::filesystem::path &inPath, + const std::filesystem::path &outPath, + const std::string &password) + { + spdlog::info("SslCertUtil: encrypting '{}' -> '{}' (AES-256-CBC + PBKDF2)", + inPath.string(), outPath.string()); + + // ----- 1) Read input file ----- + std::ifstream fin(inPath, std::ios::binary); + if (!fin) + { + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: cannot open input file: " + + inPath.string()); + } + std::vector plaintext( + (std::istreambuf_iterator(fin)), + std::istreambuf_iterator()); + fin.close(); + + // ----- 2) Prepare salt, key, iv ----- + unsigned char salt[8]; + if (RAND_bytes(salt, sizeof(salt)) != 1) + { + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: RAND_bytes failed"); + } + + const EVP_CIPHER *cipher = EVP_aes_256_cbc(); + const int keyLen = EVP_CIPHER_key_length(cipher); + const int ivLen = EVP_CIPHER_iv_length(cipher); + + std::vector keyiv(keyLen + ivLen); + + const int iterations = 10000; // PBKDF2 rounds + if (PKCS5_PBKDF2_HMAC(password.c_str(), + static_cast(password.size()), + salt, sizeof(salt), + iterations, + EVP_sha256(), + keyLen + ivLen, + keyiv.data()) != 1) + { + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: PKCS5_PBKDF2_HMAC failed"); + } + + unsigned char *key = keyiv.data(); + unsigned char *iv = keyiv.data() + keyLen; + + // ----- 3) Encrypt ----- + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + { + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_CIPHER_CTX_new failed"); + } + + std::vector ciphertext(plaintext.size() + EVP_CIPHER_block_size(cipher)); + int outLen1 = 0; + int outLen2 = 0; + + if (EVP_EncryptInit_ex(ctx, cipher, nullptr, key, iv) != 1) + { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_EncryptInit_ex failed"); + } + + if (EVP_EncryptUpdate(ctx, + ciphertext.data(), &outLen1, + plaintext.data(), static_cast(plaintext.size())) != 1) + { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_EncryptUpdate failed"); + } + + if (EVP_EncryptFinal_ex(ctx, + ciphertext.data() + outLen1, &outLen2) != 1) + { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_EncryptFinal_ex failed"); + } + + EVP_CIPHER_CTX_free(ctx); + ciphertext.resize(outLen1 + outLen2); + + // ----- 4) Write output: [salt][ciphertext] ----- + std::ofstream fout(outPath, std::ios::binary | std::ios::trunc); + if (!fout) + { + throw std::runtime_error("EncryptFileAes256CbcPbkdf2: cannot open output file: " + + outPath.string()); + } + + // Just raw salt followed by ciphertext + fout.write(reinterpret_cast(salt), sizeof(salt)); + fout.write(reinterpret_cast(ciphertext.data()), + static_cast(ciphertext.size())); + fout.close(); + + spdlog::info("SslCertUtil: encryption finished, wrote {}", outPath.string()); + } + + // Compute HMAC-SHA256 and return lowercase hex string. + // Equivalent to: + // printf %s "" | openssl dgst -sha256 -hmac "" | awk '{print $2}' + static std::string ComputeHmacSha256Hex(const std::string &key, + const std::string &data) + { + unsigned char mac[EVP_MAX_MD_SIZE]; + unsigned int macLen = 0; + + if (!HMAC(EVP_sha256(), + key.data(), static_cast(key.size()), + reinterpret_cast(data.data()), + data.size(), + mac, &macLen)) + { + throw std::runtime_error("ComputeHmacSha256Hex: HMAC(EVP_sha256) failed"); + } + + return ToHexLower(mac, macLen); + } + + // Decrypt file with AES-256-CBC + PBKDF2 + salt. + // Layout: [8-byte salt][ciphertext] + // Password is KEK (hex string from HMAC). + static void DecryptFileAes256CbcPbkdf2(const std::filesystem::path &inPath, + const std::filesystem::path &outPath, + const std::string &password) + { + spdlog::info("SslCertUtil: decrypting '{}' -> '{}' (AES-256-CBC + PBKDF2)", + inPath.string(), outPath.string()); + + // ----- 1) Read input file ----- + std::ifstream fin(inPath, std::ios::binary); + if (!fin) + { + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: cannot open input file: " + + inPath.string()); + } + std::vector enc( + (std::istreambuf_iterator(fin)), + std::istreambuf_iterator()); + fin.close(); + + if (enc.size() < 8) + { + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: encrypted file too small"); + } + + // First 8 bytes = salt, rest = ciphertext + unsigned char salt[8]; + std::copy(enc.begin(), enc.begin() + 8, salt); + std::vector ciphertext(enc.begin() + 8, enc.end()); + + // ----- 2) Derive key/iv ----- + const EVP_CIPHER *cipher = EVP_aes_256_cbc(); + const int keyLen = EVP_CIPHER_key_length(cipher); + const int ivLen = EVP_CIPHER_iv_length(cipher); + + std::vector keyiv(keyLen + ivLen); + + const int iterations = 10000; + if (PKCS5_PBKDF2_HMAC(password.c_str(), + static_cast(password.size()), + salt, sizeof(salt), + iterations, + EVP_sha256(), + keyLen + ivLen, + keyiv.data()) != 1) + { + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: PKCS5_PBKDF2_HMAC failed"); + } + + unsigned char *key = keyiv.data(); + unsigned char *iv = keyiv.data() + keyLen; + + // ----- 3) Decrypt ----- + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + { + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_CIPHER_CTX_new failed"); + } + + std::vector plaintext(ciphertext.size() + EVP_CIPHER_block_size(cipher)); + int outLen1 = 0; + int outLen2 = 0; + + if (EVP_DecryptInit_ex(ctx, cipher, nullptr, key, iv) != 1) + { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_DecryptInit_ex failed"); + } + + if (EVP_DecryptUpdate(ctx, + plaintext.data(), &outLen1, + ciphertext.data(), static_cast(ciphertext.size())) != 1) + { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_DecryptUpdate failed"); + } + + if (EVP_DecryptFinal_ex(ctx, + plaintext.data() + outLen1, &outLen2) != 1) + { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_DecryptFinal_ex failed"); + } + + EVP_CIPHER_CTX_free(ctx); + plaintext.resize(outLen1 + outLen2); + + // ----- 4) Write plaintext ----- + std::filesystem::create_directories(outPath.parent_path()); + std::ofstream fout(outPath, std::ios::binary | std::ios::trunc); + if (!fout) + { + throw std::runtime_error("DecryptFileAes256CbcPbkdf2: cannot open output file: " + + outPath.string()); + } + + fout.write(reinterpret_cast(plaintext.data()), + static_cast(plaintext.size())); + fout.close(); + + spdlog::info("SslCertUtil: decryption finished, wrote {}", outPath.string()); + } + + private: + static void BuildAndSaveCsr(EVP_PKEY *pkey, + const std::string &guid, + const std::filesystem::path &csrPath) + { + X509_REQ *req = X509_REQ_new(); + if (!req) + { + throw std::runtime_error("BuildAndSaveCsr: X509_REQ_new failed"); + } + + try + { + // Subject: CN = GUID + X509_NAME *name = X509_NAME_new(); + if (!name) + { + throw std::runtime_error("BuildAndSaveCsr: X509_NAME_new failed"); + } + if (X509_NAME_add_entry_by_NID( + name, + NID_commonName, + MBSTRING_ASC, + reinterpret_cast(guid.c_str()), + -1, -1, 0) != 1) + { + X509_NAME_free(name); + throw std::runtime_error("BuildAndSaveCsr: X509_NAME_add_entry_by_NID(CN) failed"); + } + if (X509_REQ_set_subject_name(req, name) != 1) + { + X509_NAME_free(name); + throw std::runtime_error("BuildAndSaveCsr: X509_REQ_set_subject_name failed"); + } + X509_NAME_free(name); + + // Public key + if (X509_REQ_set_pubkey(req, pkey) != 1) + { + throw std::runtime_error("BuildAndSaveCsr: X509_REQ_set_pubkey failed"); + } + + // subjectAltName = URI:urn:device: + X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, nullptr, nullptr, req, nullptr, 0); + + std::string sanStr = "URI:urn:device:" + guid; + X509_EXTENSION *ext = X509V3_EXT_conf_nid( + nullptr, &ctx, NID_subject_alt_name, + const_cast(sanStr.c_str())); + if (!ext) + { + throw std::runtime_error("BuildAndSaveCsr: X509V3_EXT_conf_nid(subjectAltName) failed"); + } + + STACK_OF(X509_EXTENSION) *exts = sk_X509_EXTENSION_new_null(); + if (!exts) + { + X509_EXTENSION_free(ext); + throw std::runtime_error("BuildAndSaveCsr: sk_X509_EXTENSION_new_null failed"); + } + sk_X509_EXTENSION_push(exts, ext); + if (X509_REQ_add_extensions(req, exts) != 1) + { + sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); + throw std::runtime_error("BuildAndSaveCsr: X509_REQ_add_extensions failed"); + } + sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); + + // Sign with SHA-256 + if (X509_REQ_sign(req, pkey, EVP_sha256()) <= 0) + { + throw std::runtime_error("BuildAndSaveCsr: X509_REQ_sign failed"); + } + + SaveCsrPem(req, csrPath); + } + catch (...) + { + X509_REQ_free(req); + throw; + } + + X509_REQ_free(req); + } + + static void SavePrivateKeyPem(EVP_PKEY *pkey, const std::filesystem::path &keyPath) + { + const auto parent = keyPath.parent_path(); + if (!parent.empty()) + { + std::filesystem::create_directories(parent); + } + + FILE *f = fopen(keyPath.string().c_str(), "wb"); + if (!f) + { + throw std::runtime_error("SavePrivateKeyPem: fopen failed: " + keyPath.string()); + } + + if (PEM_write_PrivateKey(f, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) + { + fclose(f); + throw std::runtime_error("SavePrivateKeyPem: PEM_write_PrivateKey failed"); + } + + fclose(f); + + // chmod 600 + ::chmod(keyPath.string().c_str(), 0600); + } + + static void SaveCsrPem(X509_REQ *req, const std::filesystem::path &csrPath) + { + const auto parent = csrPath.parent_path(); + if (!parent.empty()) + { + std::filesystem::create_directories(parent); + } + + FILE *f = fopen(csrPath.string().c_str(), "wb"); + if (!f) + { + throw std::runtime_error("SaveCsrPem: fopen failed: " + csrPath.string()); + } + + if (PEM_write_X509_REQ(f, req) != 1) + { + fclose(f); + throw std::runtime_error("SaveCsrPem: PEM_write_X509_REQ failed"); + } + + fclose(f); + } + + static std::string ToHexLower(const unsigned char *buf, size_t len) + { + static const char *hex = "0123456789abcdef"; + std::string out; + out.reserve(len * 2); + + for (size_t i = 0; i < len; ++i) + { + unsigned char b = buf[i]; + out.push_back(hex[b >> 4]); + out.push_back(hex[b & 0x0F]); + } + return out; + } + }; + + } // namespace device_sec +} // namespace snoop diff --git a/src/Security/TlsKeyUtil.h b/src/Security/TlsKeyUtil.h index 13ffff3..18c0ae9 100644 --- a/src/Security/TlsKeyUtil.h +++ b/src/Security/TlsKeyUtil.h @@ -9,6 +9,7 @@ #include #include #include +#include "SslCertUtil.h" namespace snoop { @@ -119,5 +120,100 @@ namespace snoop return std::vector(bytes.begin(), bytes.end()); } + + static std::string GetCpuSerial() + { + std::ifstream f("/proc/cpuinfo"); + if (!f) + { + spdlog::warn("GetCpuSerial: /proc/cpuinfo not available, using fallback"); + return std::string(12, '9'); + } + + std::string line; + std::regex re(R"(^\s*Serial\s*:\s*([0-9A-Fa-f]+)\s*$)"); + while (std::getline(f, line)) + { + std::smatch m; + if (std::regex_match(line, m, re) && m.size() == 2) + { + auto serial = Trim(m[1].str()); + if (!serial.empty()) + { + return serial; + } + } + } + spdlog::warn("GetCpuSerial: Serial not found, using fallback"); + return std::string(12, '9'); + } + + inline bool IsClientKeyInKernelKeyring() + { + std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1")); + return !id.empty(); + } + + // Ensure that iot-client-key exists in kernel keyring. + // - If already present: no-op, returns false (nothing loaded). + // - If not present: decrypts /etc/iot/keys/device.key.enc using KEK + // derived from (GUID, CPU_SERIAL), padds into keyring, returns true. + inline bool EnsureClientKeyInKernelKeyring(const std::string &guid) + { + if (IsClientKeyInKernelKeyring()) + { + spdlog::info("EnsureClientKeyInKernelKeyring: key already present, nothing to do"); + return false; + } + + const std::filesystem::path encPath = "/etc/iot/keys/device.key.enc"; + if (!std::filesystem::exists(encPath)) + { + throw std::runtime_error("EnsureClientKeyInKernelKeyring: encrypted key not found at " + + encPath.string()); + } + + // Derive KEK = HMAC-SHA256(cpuSerial, key=GUID) + const std::string cpuSerial = GetCpuSerial(); + spdlog::info("EnsureClientKeyInKernelKeyring: CPU_SERIAL = {}", cpuSerial); + + const std::string kek = + snoop::device_sec::SslCertUtil::ComputeHmacSha256Hex(guid, cpuSerial); + + if (kek.empty()) + { + throw std::runtime_error("EnsureClientKeyInKernelKeyring: empty KEK"); + } + spdlog::debug("EnsureClientKeyInKernelKeyring: KEK (hex) = {}", kek); + + // Decrypt into tmpfs: /run/iot/device.key + const std::filesystem::path runDir = "/run/iot"; + const std::filesystem::path plainPath = runDir / "device.key"; + std::filesystem::create_directories(runDir); + + snoop::device_sec::SslCertUtil::DecryptFileAes256CbcPbkdf2( + encPath, + plainPath, + kek); + + if (!std::filesystem::exists(plainPath)) + { + throw std::runtime_error("EnsureClientKeyInKernelKeyring: decrypted key not created"); + } + + // Load into kernel keyring + const std::string cmd = "keyctl padd user iot-client-key @s < " + plainPath.string(); + spdlog::info("EnsureClientKeyInKernelKeyring: loading key into kernel keyring..."); + auto out = Exec(cmd); + spdlog::debug("keyctl padd output:\n{}", out); + + // Securely erase plaintext + const std::string shredCmd = "shred -u " + plainPath.string(); + spdlog::info("EnsureClientKeyInKernelKeyring: shredding plaintext key with '{}'", shredCmd); + auto shredOut = Exec(shredCmd); + spdlog::debug("shred output:\n{}", shredOut); + + return true; + } } } \ No newline at end of file diff --git a/src/Services/EnrollmentService.h b/src/Services/EnrollmentService.h index 5f8bea6..5e48bad 100644 --- a/src/Services/EnrollmentService.h +++ b/src/Services/EnrollmentService.h @@ -20,6 +20,8 @@ #include #include "ConfigService.h" +#include "Security/SslCertUtil.h" +#include "Security/TlsKeyUtil.h" namespace snoop { @@ -125,61 +127,181 @@ namespace snoop spdlog::info("Starting first-run enrollment..."); - // 1) Run gen_device_csr.sh - { - // Assumes script is in the working dir or in PATH - const std::string cmd = "bash ./gen_device_csr.sh " + guid; - spdlog::info("Executing: {}", cmd); - auto out = Exec(cmd); - spdlog::debug("gen_device_csr.sh output:\n{}", out); - } + // // 1) Run gen_device_csr.sh + // { + // // Assumes script is in the working dir or in PATH + // const std::string cmd = "bash ./gen_device_csr.sh " + guid; + // spdlog::info("Executing: {}", cmd); + // auto out = Exec(cmd); + // spdlog::debug("gen_device_csr.sh output:\n{}", out); + // } const std::string keyName = "device_" + guid + ".key"; const std::string csrName = "device_" + guid + ".csr"; - if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName)) - { - throw std::runtime_error("CSR or key was not generated by gen_device_csr.sh"); - } + // if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName)) + // { + // throw std::runtime_error("CSR or key was not generated by gen_device_csr.sh"); + // } - // 2) CPU serial (awk '/Serial/ {print $3}' /proc/cpuinfo), if empty -> 9 * 12 + // 1) Generate key + CSR via OpenSSL (no external script) + // { + // spdlog::info("Generating device key + CSR via SslCertUtil"); + // snoop::device_sec::SslCertUtil::GenerateEcKeyAndCsr( + // guid, + // std::filesystem::path(keyName), + // std::filesystem::path(csrName)); + // } + + // if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName)) + // { + // throw std::runtime_error("CSR or key was not generated"); + // } + + // 1) CPU serial (awk '/Serial/ {print $3}' /proc/cpuinfo), if empty -> 9 * 12 const std::string cpuSerial = ParseCpuSerial(); spdlog::info("CPU_SERIAL = {}", cpuSerial); // 3) KEK = HMAC-SHA256(cpuSerial, key=GUID) (bash equiv used) + // std::string kek; + // { + // // careful to avoid trailing newline + // const std::string cmd = "printf %s \"" + cpuSerial + "\"" + // " | openssl dgst -sha256 -hmac \"" + + // guid + "\" | awk '{print $2}'"; + // spdlog::info("Deriving KEK with HMAC-SHA256"); + // kek = Trim(Exec(cmd)); + // if (kek.empty()) + // throw std::runtime_error("Failed to derive KEK"); + // spdlog::debug("KEK (hex) = {}", kek); + // } + + // 2) KEK = HMAC-SHA256(cpuSerial, key=GUID) std::string kek; { - // careful to avoid trailing newline - const std::string cmd = "printf %s \"" + cpuSerial + "\"" - " | openssl dgst -sha256 -hmac \"" + - guid + "\" | awk '{print $2}'"; - spdlog::info("Deriving KEK with HMAC-SHA256"); - kek = Trim(Exec(cmd)); + spdlog::info("Deriving KEK with HMAC-SHA256 (OpenSSL API)"); + kek = snoop::device_sec::SslCertUtil::ComputeHmacSha256Hex(guid, cpuSerial); if (kek.empty()) + { throw std::runtime_error("Failed to derive KEK"); + } spdlog::debug("KEK (hex) = {}", kek); } - // 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original + // // 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original + // { + // std::filesystem::create_directories(keystoreDir); + // const std::string cmd = + // "openssl enc -aes-256-cbc -pbkdf2 -salt " + // "-pass pass:" + + // kek + " " + // "-in " + + // keyName + " " + // "-out " + + // encKeyPath.string() + " " + // "&& shred -u " + + // keyName; + // spdlog::info("Encrypting private key and shredding plaintext..."); + // auto out = Exec(cmd); + // spdlog::debug("openssl enc output:\n{}", out); + + // if (!std::filesystem::exists(encKeyPath)) + // { + // throw std::runtime_error("Encrypted key not created: " + encKeyPath.string()); + // } + // } + + // 3) Generate key + CSR via OpenSSL (no external script) + if (!std::filesystem::exists(encKeyPath)) { + // ---- Fresh key: generate + encrypt ---- + spdlog::info("No encrypted key yet -> generating new EC key + CSR"); + snoop::device_sec::SslCertUtil::GenerateEcKeyAndCsr( + guid, + std::filesystem::path(keyName), + std::filesystem::path(csrName)); + + if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName)) + throw std::runtime_error("CSR or key was not generated"); + std::filesystem::create_directories(keystoreDir); - const std::string cmd = - "openssl enc -aes-256-cbc -pbkdf2 -salt " - "-pass pass:" + - kek + " " - "-in " + - keyName + " " - "-out " + - encKeyPath.string() + " " - "&& shred -u " + - keyName; - spdlog::info("Encrypting private key and shredding plaintext..."); - auto out = Exec(cmd); - spdlog::debug("openssl enc output:\n{}", out); + + spdlog::info("Encrypting private key via SslCertUtil (AES-256-CBC + PBKDF2)..."); + snoop::device_sec::SslCertUtil::EncryptFileAes256CbcPbkdf2( + std::filesystem::path(keyName), + encKeyPath, + kek); if (!std::filesystem::exists(encKeyPath)) - { throw std::runtime_error("Encrypted key not created: " + encKeyPath.string()); + + // shred plaintext key + const std::string shredCmd = "shred -u " + keyName; + spdlog::info("Shredding plaintext private key with '{}'", shredCmd); + auto out = Exec(shredCmd); + spdlog::debug("shred output:\n{}", out); + } + else + { + // ---- Key already exists: reuse it ---- + spdlog::info("Encrypted key already exists -> reusing key, regenerating CSR only"); + + // Decrypt to temp file (in /run/iot) + snoop::device_sec::TempFile tf(std::filesystem::path("/run/iot")); + snoop::device_sec::SslCertUtil::DecryptFileAes256CbcPbkdf2( + encKeyPath, + tf.path, + kek); + + // Generate CSR from existing key + snoop::device_sec::SslCertUtil::GenerateCsrFromExistingKey( + guid, + tf.path, + std::filesystem::path(csrName)); + + if (!std::filesystem::exists(csrName)) + throw std::runtime_error("CSR was not generated from existing key"); + + // TempFile destructor will securely delete the decrypted key file + } + + // // 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original + // { + // std::filesystem::create_directories(keystoreDir); + + // spdlog::info("Encrypting private key via SslCertUtil (AES-256-CBC + PBKDF2)..."); + // snoop::device_sec::SslCertUtil::EncryptFileAes256CbcPbkdf2( + // std::filesystem::path(keyName), + // encKeyPath, + // kek); + + // if (!std::filesystem::exists(encKeyPath)) + // { + // throw std::runtime_error("Encrypted key not created: " + encKeyPath.string()); + // } + + // // still use shred to securely remove plaintext key, as requested + // const std::string shredCmd = "shred -u " + keyName; + // spdlog::info("Shredding plaintext private key with '{}'", shredCmd); + // auto out = Exec(shredCmd); + // spdlog::debug("shred output:\n{}", out); + // } + + // 4.5) Ensure key is loaded into kernel keyring (first creation) + try + { + bool loaded = snoop::device_sec::EnsureClientKeyInKernelKeyring(guid); + if (loaded) + { + spdlog::info("EnsureEnrolled: client key loaded into kernel keyring"); } + else + { + spdlog::info("EnsureEnrolled: client key was already in keyring"); + } + } + catch (const std::exception &e) + { + spdlog::warn("EnsureEnrolled: failed to load key into keyring: {}", e.what()); } // 5) Send CSR to /enroll/:guid as multipart form, field `json: {"csr":"<...>"}` @@ -287,13 +409,31 @@ namespace snoop } // ----- 2) re-generate CSR via your bash script ----- - { - const std::string cmd = "bash ./gen_device_csr.sh " + guid; - spdlog::info("Executing (renew): {}", cmd); - auto out = Exec(cmd); - spdlog::debug("gen_device_csr.sh (renew) output:\n{}", out); - } + // { + // const std::string cmd = "bash ./gen_device_csr.sh " + guid; + // spdlog::info("Executing (renew): {}", cmd); + // auto out = Exec(cmd); + // spdlog::debug("gen_device_csr.sh (renew) output:\n{}", out); + // } + // const std::string csrName = "device_" + guid + ".csr"; + // if (!std::filesystem::exists(csrName)) + // { + // throw std::runtime_error("Renew: CSR was not generated"); + // } + // std::string csrPem = ReadFile(csrName); + + // ----- 2) re-generate CSR via OpenSSL (no bash) ----- const std::string csrName = "device_" + guid + ".csr"; + { + spdlog::info("RenewCertificate: generating new CSR via SslCertUtil"); + // We must reuse the existing key or generate a new one? + // Current flow (bash script) always regenerated key+csr, so we mimic that: + snoop::device_sec::SslCertUtil::GenerateEcKeyAndCsr( + guid, + std::filesystem::path("device_" + guid + ".key"), + std::filesystem::path(csrName)); + } + if (!std::filesystem::exists(csrName)) { throw std::runtime_error("Renew: CSR was not generated"); @@ -461,7 +601,7 @@ namespace snoop if (days == 0 && secs <= 0) { return true; - } + } return days <= daysThreshold; } diff --git a/src/main.cpp b/src/main.cpp index 20ca1ac..e1846c7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "Services/ConfigService.h" #include "Services/EnrollmentService.h" #include "Services/DeviceControlService.h" +#include "Security/TlsKeyUtil.h" #ifdef USE_ALSA_ADAPTER #include "AudioAdapters/AlsaAudioAdapter.h" @@ -35,22 +36,40 @@ namespace snoop auto configService = std::make_shared("config.json"); // ---- FIRST-RUN ENROLLMENT ---- + // EnrollmentService enroll(configService); + // const bool didEnroll = enroll.EnsureEnrolled(); + // if (didEnroll) + // { + // spdlog::info("First-run enrollment completed."); + // } + auto enrollSvc = std::make_shared(configService); + enrollSvc->EnsureEnrolled(); + try { - EnrollmentService enroll(configService); - const bool didEnroll = enroll.EnsureEnrolled(); - if (didEnroll) + enrollSvc->RenewCertificate(false); + } + catch (const std::exception &e) + { + spdlog::warn("Auto-renew check failed: {}", e.what()); + } + + // Ensure client key is in kernel keyring for this session + try + { + bool loaded = snoop::device_sec::EnsureClientKeyInKernelKeyring(configService->GetGuid()); + if (loaded) { - spdlog::info("First-run enrollment completed."); + spdlog::info("Main: client key loaded into kernel keyring for this session"); } - try + else { - enroll.RenewCertificate(false); - } - catch (const std::exception &e) - { - spdlog::warn("Auto-renew check failed: {}", e.what()); + spdlog::info("Main: client key already present in kernel keyring"); } } + catch (const std::exception &e) + { + spdlog::warn("Main: failed to ensure client key in keyring: {}", e.what()); + } auto writerService = std::make_shared(configService, "records");