changed bash scripts key/cert generation to openssl lib

This commit is contained in:
tdv
2025-11-14 16:16:40 +02:00
parent e6ed0e6d2f
commit 21088468b7
6 changed files with 821 additions and 52 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@
/toolchain/build/ /toolchain/build/
.vscode/ .vscode/
check/

View File

@@ -37,6 +37,7 @@ set( HEADERS
src/Services/DeviceControlService.h src/Services/DeviceControlService.h
src/Services/EnrollmentService.h src/Services/EnrollmentService.h
src/Security/TlsKeyUtil.h src/Security/TlsKeyUtil.h
src/Security/SslCertUtil.h
) )
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} ) add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )

512
src/Security/SslCertUtil.h Normal file
View File

@@ -0,0 +1,512 @@
// src/Security/SslCertUtil.h
#pragma once
#include <string>
#include <vector>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <sys/stat.h> // chmod
#include <openssl/evp.h>
#include <openssl/ec.h>
#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <openssl/rand.h>
#include <openssl/hmac.h>
#include <spdlog/spdlog.h>
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:<guid>
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:<password> -in in -out out
//
// NOTE: Does NOT shred or delete the input file; caller should do that
// (youll 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<unsigned char> plaintext(
(std::istreambuf_iterator<char>(fin)),
std::istreambuf_iterator<char>());
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<unsigned char> keyiv(keyLen + ivLen);
const int iterations = 10000; // PBKDF2 rounds
if (PKCS5_PBKDF2_HMAC(password.c_str(),
static_cast<int>(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<unsigned char> 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<int>(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<const char *>(salt), sizeof(salt));
fout.write(reinterpret_cast<const char *>(ciphertext.data()),
static_cast<std::streamsize>(ciphertext.size()));
fout.close();
spdlog::info("SslCertUtil: encryption finished, wrote {}", outPath.string());
}
// Compute HMAC-SHA256 and return lowercase hex string.
// Equivalent to:
// printf %s "<data>" | openssl dgst -sha256 -hmac "<key>" | 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<int>(key.size()),
reinterpret_cast<const unsigned char *>(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<unsigned char> enc(
(std::istreambuf_iterator<char>(fin)),
std::istreambuf_iterator<char>());
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<unsigned char> 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<unsigned char> keyiv(keyLen + ivLen);
const int iterations = 10000;
if (PKCS5_PBKDF2_HMAC(password.c_str(),
static_cast<int>(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<unsigned char> 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<int>(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<const char *>(plaintext.data()),
static_cast<std::streamsize>(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<const unsigned char *>(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:<GUID>
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<char *>(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

View File

@@ -9,6 +9,7 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <fcntl.h> #include <fcntl.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "SslCertUtil.h"
namespace snoop namespace snoop
{ {
@@ -119,5 +120,100 @@ namespace snoop
return std::vector<uint8_t>(bytes.begin(), bytes.end()); return std::vector<uint8_t>(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;
}
} }
} }

View File

@@ -20,6 +20,8 @@
#include <openssl/asn1.h> #include <openssl/asn1.h>
#include "ConfigService.h" #include "ConfigService.h"
#include "Security/SslCertUtil.h"
#include "Security/TlsKeyUtil.h"
namespace snoop namespace snoop
{ {
@@ -125,61 +127,181 @@ namespace snoop
spdlog::info("Starting first-run enrollment..."); spdlog::info("Starting first-run enrollment...");
// 1) Run gen_device_csr.sh <GUID> // // 1) Run gen_device_csr.sh <GUID>
{ // {
// Assumes script is in the working dir or in PATH // // Assumes script is in the working dir or in PATH
const std::string cmd = "bash ./gen_device_csr.sh " + guid; // const std::string cmd = "bash ./gen_device_csr.sh " + guid;
spdlog::info("Executing: {}", cmd); // spdlog::info("Executing: {}", cmd);
auto out = Exec(cmd); // auto out = Exec(cmd);
spdlog::debug("gen_device_csr.sh output:\n{}", out); // spdlog::debug("gen_device_csr.sh output:\n{}", out);
} // }
const std::string keyName = "device_" + guid + ".key"; const std::string keyName = "device_" + guid + ".key";
const std::string csrName = "device_" + guid + ".csr"; const std::string csrName = "device_" + guid + ".csr";
if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName)) // if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName))
{ // {
throw std::runtime_error("CSR or key was not generated by gen_device_csr.sh"); // 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(); const std::string cpuSerial = ParseCpuSerial();
spdlog::info("CPU_SERIAL = {}", cpuSerial); spdlog::info("CPU_SERIAL = {}", cpuSerial);
// 3) KEK = HMAC-SHA256(cpuSerial, key=GUID) (bash equiv used) // 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; std::string kek;
{ {
// careful to avoid trailing newline spdlog::info("Deriving KEK with HMAC-SHA256 (OpenSSL API)");
const std::string cmd = "printf %s \"" + cpuSerial + "\"" kek = snoop::device_sec::SslCertUtil::ComputeHmacSha256Hex(guid, cpuSerial);
" | openssl dgst -sha256 -hmac \"" +
guid + "\" | awk '{print $2}'";
spdlog::info("Deriving KEK with HMAC-SHA256");
kek = Trim(Exec(cmd));
if (kek.empty()) if (kek.empty())
{
throw std::runtime_error("Failed to derive KEK"); throw std::runtime_error("Failed to derive KEK");
}
spdlog::debug("KEK (hex) = {}", 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); std::filesystem::create_directories(keystoreDir);
const std::string cmd =
"openssl enc -aes-256-cbc -pbkdf2 -salt " spdlog::info("Encrypting private key via SslCertUtil (AES-256-CBC + PBKDF2)...");
"-pass pass:" + snoop::device_sec::SslCertUtil::EncryptFileAes256CbcPbkdf2(
kek + " " std::filesystem::path(keyName),
"-in " + encKeyPath,
keyName + " " kek);
"-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)) if (!std::filesystem::exists(encKeyPath))
{
throw std::runtime_error("Encrypted key not created: " + encKeyPath.string()); 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":"<...>"}` // 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 ----- // ----- 2) re-generate CSR via your bash script -----
{ // {
const std::string cmd = "bash ./gen_device_csr.sh " + guid; // const std::string cmd = "bash ./gen_device_csr.sh " + guid;
spdlog::info("Executing (renew): {}", cmd); // spdlog::info("Executing (renew): {}", cmd);
auto out = Exec(cmd); // auto out = Exec(cmd);
spdlog::debug("gen_device_csr.sh (renew) output:\n{}", out); // 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"; 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)) if (!std::filesystem::exists(csrName))
{ {
throw std::runtime_error("Renew: CSR was not generated"); throw std::runtime_error("Renew: CSR was not generated");

View File

@@ -10,6 +10,7 @@
#include "Services/ConfigService.h" #include "Services/ConfigService.h"
#include "Services/EnrollmentService.h" #include "Services/EnrollmentService.h"
#include "Services/DeviceControlService.h" #include "Services/DeviceControlService.h"
#include "Security/TlsKeyUtil.h"
#ifdef USE_ALSA_ADAPTER #ifdef USE_ALSA_ADAPTER
#include "AudioAdapters/AlsaAudioAdapter.h" #include "AudioAdapters/AlsaAudioAdapter.h"
@@ -35,22 +36,40 @@ namespace snoop
auto configService = std::make_shared<ConfigService>("config.json"); auto configService = std::make_shared<ConfigService>("config.json");
// ---- FIRST-RUN ENROLLMENT ---- // ---- FIRST-RUN ENROLLMENT ----
// EnrollmentService enroll(configService);
// const bool didEnroll = enroll.EnsureEnrolled();
// if (didEnroll)
// {
// spdlog::info("First-run enrollment completed.");
// }
auto enrollSvc = std::make_shared<EnrollmentService>(configService);
enrollSvc->EnsureEnrolled();
try
{ {
EnrollmentService enroll(configService); enrollSvc->RenewCertificate(false);
const bool didEnroll = enroll.EnsureEnrolled(); }
if (didEnroll) 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); spdlog::info("Main: client key already present in kernel keyring");
}
catch (const std::exception &e)
{
spdlog::warn("Auto-renew check failed: {}", e.what());
} }
} }
catch (const std::exception &e)
{
spdlog::warn("Main: failed to ensure client key in keyring: {}", e.what());
}
auto writerService = std::make_shared<AudioWriterService>(configService, "records"); auto writerService = std::make_shared<AudioWriterService>(configService, "records");