Compare commits

..

3 Commits

8 changed files with 996 additions and 82 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
{ {
@@ -20,7 +21,9 @@ namespace snoop
auto b = s.find_first_not_of(" \t\r\n"); auto b = s.find_first_not_of(" \t\r\n");
auto e = s.find_last_not_of(" \t\r\n"); auto e = s.find_last_not_of(" \t\r\n");
if (b == std::string::npos) if (b == std::string::npos)
{
return ""; return "";
}
return s.substr(b, e - b + 1); return s.substr(b, e - b + 1);
} }
@@ -30,13 +33,19 @@ namespace snoop
std::string out; std::string out;
FILE *pipe = popen((cmd + " 2>&1").c_str(), "r"); FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
if (!pipe) if (!pipe)
{
throw std::runtime_error("popen failed: " + cmd); throw std::runtime_error("popen failed: " + cmd);
}
while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr) while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr)
{
out.append(buf.data()); out.append(buf.data());
}
int rc = pclose(pipe); int rc = pclose(pipe);
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc; int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
if (exitCode != 0) if (exitCode != 0)
{
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode); spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
}
return out; return out;
} }
@@ -45,12 +54,16 @@ namespace snoop
{ {
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1")); std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
if (id.empty()) if (id.empty())
{
throw std::runtime_error("iot-client-key not found in keyring"); throw std::runtime_error("iot-client-key not found in keyring");
}
// Create a secure temp file // Create a secure temp file
char tmpl[] = "/run/iot/iot-keyXXXXXX"; char tmpl[] = "/run/iot/iot-keyXXXXXX";
int fd = mkstemp(tmpl); int fd = mkstemp(tmpl);
if (fd < 0) if (fd < 0)
{
throw std::runtime_error("mkstemp failed for client key"); throw std::runtime_error("mkstemp failed for client key");
}
close(fd); close(fd);
std::filesystem::path p(tmpl); std::filesystem::path p(tmpl);
@@ -79,7 +92,9 @@ namespace snoop
name.push_back('\0'); name.push_back('\0');
fd = mkstemp(name.data()); fd = mkstemp(name.data());
if (fd < 0) if (fd < 0)
{
throw std::runtime_error("mkstemp failed"); throw std::runtime_error("mkstemp failed");
}
fchmod(fd, S_IRUSR | S_IWUSR); fchmod(fd, S_IRUSR | S_IWUSR);
path = name.data(); path = name.data();
} }
@@ -91,7 +106,9 @@ namespace snoop
{ {
ssize_t w = ::write(fd, p + off, n - off); ssize_t w = ::write(fd, p + off, n - off);
if (w <= 0) if (w <= 0)
{
throw std::runtime_error("write failed"); throw std::runtime_error("write failed");
}
off += (size_t)w; off += (size_t)w;
} }
fsync(fd); fsync(fd);
@@ -99,7 +116,9 @@ namespace snoop
~TempFile() ~TempFile()
{ {
if (fd >= 0) if (fd >= 0)
{
::close(fd); ::close(fd);
}
std::error_code ec; std::error_code ec;
std::filesystem::remove(path, ec); std::filesystem::remove(path, ec);
} }
@@ -110,14 +129,113 @@ namespace snoop
// 1) get key id // 1) get key id
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1")); std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
if (id.empty()) if (id.empty())
{
throw std::runtime_error("iot-client-key not found in keyring"); throw std::runtime_error("iot-client-key not found in keyring");
}
// 2) capture payload (no redirection to file) // 2) capture payload (no redirection to file)
std::string bytes = Exec("keyctl pipe " + id); std::string bytes = Exec("keyctl pipe " + id);
if (bytes.empty()) if (bytes.empty())
{
throw std::runtime_error("keyctl pipe returned empty payload"); throw std::runtime_error("keyctl pipe returned empty payload");
}
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

@@ -135,9 +135,11 @@ namespace snoop
spdlog::info("StopRecordingNow ignored: not recording"); spdlog::info("StopRecordingNow ignored: not recording");
return; return;
} }
auto stoppedAtMs = NowMs();
// Force-close current segment right away, enqueue, and disable recording. // Force-close current segment right away, enqueue, and disable recording.
this->m_oggWriter->StopWriting(); this->m_oggWriter->StopWriting();
this->MoveToUploadQueue(this->m_currentRecordFilePath); this->MoveToUploadQueue(this->m_currentRecordFilePath,this->m_currentRecordStartedAt,stoppedAtMs);
m_recordingEnabled = false; m_recordingEnabled = false;
m_stopAfterCurrentSegment = false; m_stopAfterCurrentSegment = false;
spdlog::info("Recording stopped immediately (deep sleep)"); spdlog::info("Recording stopped immediately (deep sleep)");
@@ -207,7 +209,9 @@ namespace snoop
// Setup CA/CRT (from enrollment) // Setup CA/CRT (from enrollment)
std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem";
if (!std::filesystem::exists(ca)) if (!std::filesystem::exists(ca))
{
ca = "/etc/iot/keys/ca_chain.pem"; ca = "/etc/iot/keys/ca_chain.pem";
}
const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
if (!needReinit) if (!needReinit)
@@ -262,7 +266,8 @@ namespace snoop
void WritingThread() void WritingThread()
{ {
// recording starts ONLY when StartRecording() is called constexpr unsigned long long DEFAULT_SEGMENT_MS = 30'000; // 30 seconds
while (!m_isIntermission) while (!m_isIntermission)
{ {
if (!m_recordingEnabled.load()) if (!m_recordingEnabled.load())
@@ -271,32 +276,52 @@ namespace snoop
continue; continue;
} }
// Start a fresh segment // --- Get segment duration from config ---
auto now = std::chrono::system_clock::now(); unsigned long long segDurationMs = this->m_configService->GetRecordingDuration();
if (segDurationMs < 1000) // anything < 1s is basically useless here
{
spdlog::warn("RecordingDuration={} ms is too small, using default {} ms",
segDurationMs, DEFAULT_SEGMENT_MS);
segDurationMs = DEFAULT_SEGMENT_MS;
}
spdlog::info("Starting new segment with target duration={} ms", segDurationMs);
// --- Mark start times (wall + monotonic) ---
auto wallStart = std::chrono::system_clock::now();
this->m_currentRecordStartedAt = this->m_currentRecordStartedAt =
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count(); std::chrono::duration_cast<std::chrono::milliseconds>(wallStart.time_since_epoch()).count();
this->m_currentRecordFilePath = this->m_destinationDirectoryPath + std::to_string(this->m_currentRecordStartedAt);
this->m_currentRecordFilePath =
this->m_destinationDirectoryPath + std::to_string(this->m_currentRecordStartedAt);
this->m_oggWriter->StartWriting(this->m_currentRecordFilePath); this->m_oggWriter->StartWriting(this->m_currentRecordFilePath);
spdlog::info("Recording segment started: {}", this->m_currentRecordFilePath); spdlog::info("Recording segment started: {}", this->m_currentRecordFilePath);
// Write until duration elapses auto monoStart = std::chrono::steady_clock::now();
const auto segDurationMs = this->m_configService->GetRecordingDuration(); const auto targetDuration = std::chrono::milliseconds(segDurationMs);
// --- Wait until segment duration elapses ---
while (!m_isIntermission && m_recordingEnabled.load()) while (!m_isIntermission && m_recordingEnabled.load())
{ {
now = std::chrono::system_clock::now(); auto elapsed = std::chrono::steady_clock::now() - monoStart;
auto currentRecordDuration = if (elapsed >= targetDuration)
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() - this->m_currentRecordStartedAt;
if (currentRecordDuration >= segDurationMs)
{
break; break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::this_thread::sleep_for(std::chrono::milliseconds(50));
} }
// Close current segment and enqueue auto wallStop = std::chrono::system_clock::now();
auto stoppedAtMs =
std::chrono::duration_cast<std::chrono::milliseconds>(wallStop.time_since_epoch()).count();
// --- Close and enqueue ---
this->m_oggWriter->StopWriting(); this->m_oggWriter->StopWriting();
this->MoveToUploadQueue(this->m_currentRecordFilePath); this->MoveToUploadQueue(this->m_currentRecordFilePath,
spdlog::info("Recording segment finished: {}", this->m_currentRecordFilePath); this->m_currentRecordStartedAt,
stoppedAtMs);
spdlog::info("Recording segment finished: {} ({} ms)",
this->m_currentRecordFilePath,
stoppedAtMs - this->m_currentRecordStartedAt);
// If graceful stop requested, stop after finishing this segment // If graceful stop requested, stop after finishing this segment
if (m_stopAfterCurrentSegment.load()) if (m_stopAfterCurrentSegment.load())
@@ -307,28 +332,58 @@ namespace snoop
} }
} }
// If exiting service while in a middle of a segment, ensure clean close // Clean shutdown if we exit while still recording
if (m_recordingEnabled.load()) if (m_recordingEnabled.load())
{ {
auto wallStop = std::chrono::system_clock::now();
auto stoppedAtMs =
std::chrono::duration_cast<std::chrono::milliseconds>(wallStop.time_since_epoch()).count();
this->m_oggWriter->StopWriting(); this->m_oggWriter->StopWriting();
this->MoveToUploadQueue(this->m_currentRecordFilePath); this->MoveToUploadQueue(this->m_currentRecordFilePath,
this->m_currentRecordStartedAt,
stoppedAtMs);
m_recordingEnabled = false; m_recordingEnabled = false;
} }
} }
// Helper: ms since epoch
static unsigned long long NowMs()
{
auto now = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
}
void MoveToUploadQueue(const std::string &filePath) void MoveToUploadQueue(const std::string &filePath, unsigned long long startedAt, unsigned long long stoppedAt)
{ {
spdlog::info("MoveToUploadQueue( {} )", filePath); spdlog::info("MoveToUploadQueue( {} )", filePath);
std::lock_guard lock(this->m_fetchFilePathsMutex); std::lock_guard lock(this->m_fetchFilePathsMutex);
auto now = std::chrono::system_clock::now();
auto recordStoppedAt = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
if (std::filesystem::exists(filePath)) if (std::filesystem::exists(filePath))
{ {
auto fileName = std::filesystem::path(filePath).filename().string() + "-" + std::to_string(recordStoppedAt); auto fileName = std::to_string(startedAt) + "-" + std::to_string(stoppedAt);
std::filesystem::rename(filePath, m_queueDirectoryPath + fileName); std::filesystem::rename(filePath, m_queueDirectoryPath + fileName);
} }
} }
// Legacy helper for startup recovery
void MoveToUploadQueue(const std::string &filePath)
{
const auto stoppedAt = NowMs();
unsigned long long startedAt = 0;
auto base = std::filesystem::path(filePath).filename().string();
try
{
startedAt = std::stoull(base);
}
catch (...)
{
// if filename is not pure number, fall back to stoppedAt
startedAt = stoppedAt;
}
MoveToUploadQueue(filePath, startedAt, stoppedAt);
}
void UploadThread() void UploadThread()
{ {
const auto baseUrl = this->m_configService->GetBaseUrl(); const auto baseUrl = this->m_configService->GetBaseUrl();
@@ -337,7 +392,9 @@ namespace snoop
// certs from enrollment // certs from enrollment
std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem";
if (!std::filesystem::exists(ca)) if (!std::filesystem::exists(ca))
{
ca = "/etc/iot/keys/ca_chain.pem"; ca = "/etc/iot/keys/ca_chain.pem";
}
const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
while (!m_isIntermission) while (!m_isIntermission)

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
{ {
@@ -34,7 +36,9 @@ namespace snoop
std::string out; std::string out;
FILE *pipe = popen((cmd + " 2>&1").c_str(), "r"); FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
if (!pipe) if (!pipe)
{
throw std::runtime_error("popen failed: " + cmd); throw std::runtime_error("popen failed: " + cmd);
}
while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr) while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr)
{ {
out.append(buf.data()); out.append(buf.data());
@@ -42,17 +46,23 @@ namespace snoop
auto rc = pclose(pipe); auto rc = pclose(pipe);
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc; int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
if (exitCode != 0) if (exitCode != 0)
{
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode); spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
}
return out; return out;
} }
static void WriteFile(const std::filesystem::path &p, const std::string &data, bool createParents = true) static void WriteFile(const std::filesystem::path &p, const std::string &data, bool createParents = true)
{ {
if (createParents) if (createParents)
{
std::filesystem::create_directories(p.parent_path()); std::filesystem::create_directories(p.parent_path());
}
std::ofstream f(p, std::ios::binary); std::ofstream f(p, std::ios::binary);
if (!f) if (!f)
{
throw std::runtime_error("Cannot open file for write: " + p.string()); throw std::runtime_error("Cannot open file for write: " + p.string());
}
f.write(data.data(), (std::streamsize)data.size()); f.write(data.data(), (std::streamsize)data.size());
} }
@@ -60,7 +70,9 @@ namespace snoop
{ {
std::ifstream f(p, std::ios::binary); std::ifstream f(p, std::ios::binary);
if (!f) if (!f)
{
throw std::runtime_error("Cannot open file for read: " + p.string()); throw std::runtime_error("Cannot open file for read: " + p.string());
}
std::ostringstream ss; std::ostringstream ss;
ss << f.rdbuf(); ss << f.rdbuf();
return ss.str(); return ss.str();
@@ -95,7 +107,9 @@ namespace snoop
{ {
auto serial = Trim(m[1].str()); auto serial = Trim(m[1].str());
if (!serial.empty()) if (!serial.empty())
{
return serial; return serial;
}
} }
} }
spdlog::warn("CPU Serial not found, using fallback"); spdlog::warn("CPU Serial not found, using fallback");
@@ -125,61 +139,185 @@ 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":"<...>"}`
@@ -198,10 +336,15 @@ namespace snoop
spdlog::info("POST {} (multipart)", path); spdlog::info("POST {} (multipart)", path);
auto res = cli.Post(path.c_str(), items); auto res = cli.Post(path.c_str(), items);
if (!res) if (!res)
{
throw std::runtime_error("Enroll request failed (no response)"); throw std::runtime_error("Enroll request failed (no response)");
}
spdlog::info("Enroll response status: {}", res->status); spdlog::info("Enroll response status: {}", res->status);
if (res->status != 200 && res->status != 201) if (res->status != 200 && res->status != 201)
{
spdlog::info("Enroll error: " + res->body);
throw std::runtime_error("Enroll failed: HTTP " + std::to_string(res->status)); throw std::runtime_error("Enroll failed: HTTP " + std::to_string(res->status));
}
// 6) Expect JSON: { "certificate": "...", "issuing_ca":"...", "ca_chain":"..." } // 6) Expect JSON: { "certificate": "...", "issuing_ca":"...", "ca_chain":"..." }
nlohmann::json j; nlohmann::json j;
@@ -214,12 +357,16 @@ namespace snoop
throw std::runtime_error(std::string("Enroll: invalid JSON: ") + e.what()); throw std::runtime_error(std::string("Enroll: invalid JSON: ") + e.what());
} }
if (!j.contains("certificate") || !j["certificate"].is_string()) if (!j.contains("certificate") || !j["certificate"].is_string())
{
throw std::runtime_error("Enroll response missing or invalid 'certificate'"); throw std::runtime_error("Enroll response missing or invalid 'certificate'");
}
WriteFile(certPath, j["certificate"].get<std::string>()); WriteFile(certPath, j["certificate"].get<std::string>());
if (j.contains("issuing_ca")) if (j.contains("issuing_ca"))
{ {
if (!j["issuing_ca"].is_string()) if (!j["issuing_ca"].is_string())
{
throw std::runtime_error("'issuing_ca' must be a string PEM"); throw std::runtime_error("'issuing_ca' must be a string PEM");
}
WriteFile(caPath, j["issuing_ca"].get<std::string>()); WriteFile(caPath, j["issuing_ca"].get<std::string>());
} }
@@ -287,13 +434,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");
@@ -315,15 +480,21 @@ namespace snoop
auto res = cli->Post(path.c_str(), items); auto res = cli->Post(path.c_str(), items);
if (!res) if (!res)
{
throw std::runtime_error("Renew request failed (no response)"); throw std::runtime_error("Renew request failed (no response)");
}
spdlog::info("Renew response status: {}", res->status); spdlog::info("Renew response status: {}", res->status);
if (res->status != 200) if (res->status != 200)
{
throw std::runtime_error("Renew failed: HTTP " + std::to_string(res->status)); throw std::runtime_error("Renew failed: HTTP " + std::to_string(res->status));
}
nlohmann::json j = nlohmann::json::parse(res->body); nlohmann::json j = nlohmann::json::parse(res->body);
if (!j.contains("certificate") || !j["certificate"].is_string()) if (!j.contains("certificate") || !j["certificate"].is_string())
{
throw std::runtime_error("Renew response missing or invalid 'certificate'"); throw std::runtime_error("Renew response missing or invalid 'certificate'");
}
WriteFile(certPath, j["certificate"].get<std::string>()); WriteFile(certPath, j["certificate"].get<std::string>());
if (j.contains("issuing_ca") && j["issuing_ca"].is_string()) if (j.contains("issuing_ca") && j["issuing_ca"].is_string())
@@ -335,11 +506,17 @@ namespace snoop
{ {
std::string chainPem; std::string chainPem;
if (j["ca_chain"].is_string()) if (j["ca_chain"].is_string())
{
chainPem = j["ca_chain"].get<std::string>(); chainPem = j["ca_chain"].get<std::string>();
}
else if (j["ca_chain"].is_array()) else if (j["ca_chain"].is_array())
{
chainPem = JoinPemArray(j["ca_chain"]); chainPem = JoinPemArray(j["ca_chain"]);
}
else else
{
throw std::runtime_error("'ca_chain' must be string or array"); throw std::runtime_error("'ca_chain' must be string or array");
}
WriteFile(chainPath, chainPem); WriteFile(chainPath, chainPem);
} }
@@ -385,7 +562,9 @@ namespace snoop
{ {
std::ifstream f(path, std::ios::binary); std::ifstream f(path, std::ios::binary);
if (!f) if (!f)
{
throw std::runtime_error("Failed to open file: " + path); throw std::runtime_error("Failed to open file: " + path);
}
std::ostringstream ss; std::ostringstream ss;
ss << f.rdbuf(); ss << f.rdbuf();
return ss.str(); return ss.str();

View File

@@ -49,7 +49,9 @@ namespace snoop
{ {
std::lock_guard lk(m_mtx); std::lock_guard lk(m_mtx);
if (m_started) if (m_started)
{
return; return;
}
// Parse & normalize the provided URL now (extract token, add trailing /whip) // Parse & normalize the provided URL now (extract token, add trailing /whip)
m_endpoint = ParseWhipUrl(m_p.whipUrl); m_endpoint = ParseWhipUrl(m_p.whipUrl);
@@ -97,11 +99,12 @@ namespace snoop
spdlog::warn("WHIP PATCH candidate failed: {}", e.what()); spdlog::warn("WHIP PATCH candidate failed: {}", e.what());
} }); } });
// 2) When ICE gathering completes, send end-of-candidates // 2) When ICE gathering completes, send end-of-candidates
m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s) m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s)
{ {
spdlog::info("WHIP gathering state: {}", (int)s); spdlog::info("WHIP gathering state: {}", (int)s);
if (s == rtc::PeerConnection::GatheringState::Complete) { if (s == rtc::PeerConnection::GatheringState::Complete)
{
PatchSdpFrag("a=end-of-candidates"); PatchSdpFrag("a=end-of-candidates");
} }); } });
@@ -136,7 +139,9 @@ namespace snoop
{ {
std::lock_guard lk(m_mtx); std::lock_guard lk(m_mtx);
if (!m_started) if (!m_started)
{
return; return;
}
m_trackOpen = false; m_trackOpen = false;
m_track.reset(); m_track.reset();
@@ -150,7 +155,9 @@ namespace snoop
{ {
std::lock_guard lk(m_mtx); std::lock_guard lk(m_mtx);
if (!m_track || !m_started || !m_trackOpen.load()) if (!m_track || !m_started || !m_trackOpen.load())
{
return; return;
}
std::array<uint8_t, 12> rtp{}; std::array<uint8_t, 12> rtp{};
rtp[0] = 0x80; // V=2 rtp[0] = 0x80; // V=2
@@ -233,7 +240,9 @@ namespace snoop
{ {
size_t amp = query.find('&', pos); size_t amp = query.find('&', pos);
if (amp == std::string::npos) if (amp == std::string::npos)
{
amp = query.size(); amp = query.size();
}
auto kv = query.substr(pos, amp - pos); auto kv = query.substr(pos, amp - pos);
auto eq = kv.find('='); auto eq = kv.find('=');
if (eq != std::string::npos) if (eq != std::string::npos)
@@ -252,9 +261,13 @@ namespace snoop
// MediaMTX wants: /whip/<original...>/whip // MediaMTX wants: /whip/<original...>/whip
// Your incoming URLs already start with /whip/... — we just ensure they end with /whip. // Your incoming URLs already start with /whip/... — we just ensure they end with /whip.
if (rawPath.empty()) if (rawPath.empty())
{
rawPath = "/"; rawPath = "/";
}
if (rawPath.back() == '/') if (rawPath.back() == '/')
{
rawPath.pop_back(); rawPath.pop_back();
}
if (rawPath.rfind("/whip", std::string::npos) != rawPath.size() - 5) if (rawPath.rfind("/whip", std::string::npos) != rawPath.size() - 5)
{ {
rawPath += "/whip"; rawPath += "/whip";
@@ -266,7 +279,9 @@ namespace snoop
static std::tuple<std::string, std::string> ExtractAnswerAndLocation(const httplib::Result &r) static std::tuple<std::string, std::string> ExtractAnswerAndLocation(const httplib::Result &r)
{ {
if (!r) if (!r)
{
throw std::runtime_error("No HTTP result"); throw std::runtime_error("No HTTP result");
}
if (r->status != 201 && r->status != 200) if (r->status != 201 && r->status != 200)
{ {
throw std::runtime_error("Unexpected WHIP status: " + std::to_string(r->status)); throw std::runtime_error("Unexpected WHIP status: " + std::to_string(r->status));
@@ -274,7 +289,9 @@ namespace snoop
std::string answer = r->body; std::string answer = r->body;
std::string resourceUrl; std::string resourceUrl;
if (r->has_header("Location")) if (r->has_header("Location"))
{
resourceUrl = r->get_header_value("Location"); resourceUrl = r->get_header_value("Location");
}
return {answer, resourceUrl}; return {answer, resourceUrl};
} }
@@ -294,7 +311,9 @@ namespace snoop
auto res = cli->Post(m_endpoint.path.c_str(), hs, sdpOffer, "application/sdp"); auto res = cli->Post(m_endpoint.path.c_str(), hs, sdpOffer, "application/sdp");
if (!res) if (!res)
{
throw std::runtime_error("No HTTP result (network?)"); throw std::runtime_error("No HTTP result (network?)");
}
const auto ctype = res->get_header_value("Content-Type"); const auto ctype = res->get_header_value("Content-Type");
const bool has_loc = res->has_header("Location"); const bool has_loc = res->has_header("Location");
@@ -313,7 +332,9 @@ namespace snoop
std::string answer = res->body; std::string answer = res->body;
std::string resourceUrl; std::string resourceUrl;
if (res->has_header("Location")) if (res->has_header("Location"))
{
resourceUrl = res->get_header_value("Location"); resourceUrl = res->get_header_value("Location");
}
if (answer.find("a=ice-ufrag:") == std::string::npos) if (answer.find("a=ice-ufrag:") == std::string::npos)
{ {
@@ -334,7 +355,9 @@ namespace snoop
// If MediaMTX returns an absolute Location for the resource, we parse it; // If MediaMTX returns an absolute Location for the resource, we parse it;
// otherwise we reuse the same host/port and use Location as path. // otherwise we reuse the same host/port and use Location as path.
if (!m_resourceUrl) if (!m_resourceUrl)
{
return; return;
}
ParsedUrl target; ParsedUrl target;
try try
@@ -393,7 +416,9 @@ namespace snoop
void PatchSdpFrag(const std::string &sdpfrag) void PatchSdpFrag(const std::string &sdpfrag)
{ {
if (!m_resourceUrl) if (!m_resourceUrl)
{
return; return;
}
// Reuse the same host/port and just set path = Location // Reuse the same host/port and just set path = Location
ParsedUrl target = m_endpoint; ParsedUrl target = m_endpoint;
@@ -411,7 +436,9 @@ namespace snoop
// MUST include CRLF at end of body // MUST include CRLF at end of body
std::string body = sdpfrag; std::string body = sdpfrag;
if (body.empty() || body.back() != '\n') if (body.empty() || body.back() != '\n')
{
body += "\r\n"; body += "\r\n";
}
auto res = cli->Patch(target.path.c_str(), hs, body, "application/trickle-ice-sdpfrag"); auto res = cli->Patch(target.path.c_str(), hs, body, "application/trickle-ice-sdpfrag");
if (!res || !(res->status == 200 || res->status == 201 || res->status == 204)) if (!res || !(res->status == 200 || res->status == 201 || res->status == 204))

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");