added certificate renewal, cleared formatting of code

This commit is contained in:
tdv
2025-10-30 16:32:49 +02:00
parent 4d055c343a
commit e6ed0e6d2f
6 changed files with 378 additions and 85 deletions

View File

@@ -19,41 +19,53 @@
#include "ConfigService.h"
#include "Security/TlsKeyUtil.h"
namespace snoop {
namespace snoop
{
class AudioStreamService {
class AudioStreamService
{
std::shared_ptr<ConfigService> m_cfg;
// WHIP
std::unique_ptr<WhipClient> m_whip;
std::mutex m_whipMutex;
public:
public:
explicit AudioStreamService(std::shared_ptr<ConfigService> cfg)
: m_cfg(std::move(cfg)) {}
~AudioStreamService() { StopWhip(); }
// Feed encoded Opus; frames = PCM samples per channel represented by this Opus frame
void OnOpus(const unsigned char* opusData, size_t opusBytes, int pcmFramesPerChannel) {
void OnOpus(const unsigned char *opusData, size_t opusBytes, int pcmFramesPerChannel)
{
std::lock_guard lk(m_whipMutex);
if (m_whip) m_whip->PushOpus(opusData, opusBytes, pcmFramesPerChannel);
if (m_whip)
{
m_whip->PushOpus(opusData, opusBytes, pcmFramesPerChannel);
}
}
bool StartWhip(const std::string& whipUrl, int sampleRate = 48000, int channels = 1) {
bool StartWhip(const std::string &whipUrl, int sampleRate = 48000, int channels = 1)
{
std::lock_guard lk(m_whipMutex);
if (m_whip) {
if (m_whip)
{
spdlog::info("WHIP already started");
return true;
}
if (!m_cfg) {
if (!m_cfg)
{
spdlog::error("StartWhip requires ConfigService");
return false;
}
// certs from enrollment
std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem";
if (!std::filesystem::exists(ca)) ca = "/etc/iot/keys/ca_chain.pem";
if (!std::filesystem::exists(ca))
{
ca = "/etc/iot/keys/ca_chain.pem";
}
std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
WhipClient::Params p{
@@ -61,28 +73,32 @@ public:
.caPath = ca.string(),
.crtPath = crt.string(),
.sampleRate = sampleRate,
.channels = channels
};
.channels = channels};
m_whip = std::make_unique<WhipClient>(p);
try {
try
{
m_whip->Start();
spdlog::info("WHIP started");
return true;
} catch (const std::exception& e) {
}
catch (const std::exception &e)
{
spdlog::error("WHIP start failed: {}", e.what());
m_whip.reset();
return false;
}
}
void StopWhip() {
void StopWhip()
{
std::lock_guard lk(m_whipMutex);
if (m_whip) {
if (m_whip)
{
m_whip->Stop();
m_whip.reset();
}
}
};
};
} // namespace snoop

View File

@@ -73,10 +73,14 @@ namespace snoop
{
this->m_isIntermission = true;
if (this->m_writingThread.joinable())
{
this->m_writingThread.join();
}
if (this->m_uploadThread.joinable())
{
this->m_uploadThread.join();
}
}
// -------- Public control API (called from DeviceControlService handlers) --------
@@ -116,7 +120,9 @@ namespace snoop
void WriteAudioData(const char *data, size_t size, size_t frames)
{
if (!m_recordingEnabled.load())
{
return;
}
this->m_oggWriter->Write(data, size, frames);
}
@@ -176,7 +182,6 @@ namespace snoop
{
if (u.scheme == "https")
{
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port, crt.string().c_str(), key.string().c_str(), std::string());
cli->enable_server_certificate_verification(false);
cli->set_ca_cert_path(ca.string().c_str());
@@ -184,9 +189,6 @@ namespace snoop
cli->set_read_timeout(120);
cli->set_write_timeout(120);
return cli;
#else
throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled");
#endif
}
}
@@ -213,8 +215,6 @@ namespace snoop
// still ok — reuse
return true;
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
try
{
auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring();
@@ -239,12 +239,6 @@ namespace snoop
spdlog::error("EnsureUploadClient: failed to set client cert: {}", e.what());
return false;
}
#else
(void)ca;
(void)crt;
spdlog::error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT not enabled");
return false;
#endif
}
// ----------------------------- Existing logic (adjusted) -----------------------------
@@ -293,7 +287,9 @@ namespace snoop
auto currentRecordDuration =
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() - this->m_currentRecordStartedAt;
if (currentRecordDuration >= segDurationMs)
{
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
@@ -354,9 +350,11 @@ namespace snoop
for (const auto &entry : std::filesystem::directory_iterator(this->m_queueDirectoryPath))
{
if (entry.is_regular_file())
{
files.push_back(entry.path());
}
}
}
catch (const std::exception &e)
{
spdlog::error("Error reading queue directory: {}", e.what());
@@ -422,8 +420,9 @@ namespace snoop
spdlog::info("SendRecordedFile (mTLS): {}", filepath);
std::ifstream ifs(filepath, std::ios::binary);
if (!ifs)
{
throw std::runtime_error("Failed to open file: " + filepath);
}
std::vector<char> buffer((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
// Multipart form: file + guid + times (same fields as before)

View File

@@ -49,6 +49,7 @@ namespace snoop
TaskHandler onStopRecording;
TaskHandler onUpdateConfig;
TaskHandler onSetDeepSleep;
TaskHandler onRenewCert;
};
struct Controls
@@ -68,8 +69,10 @@ namespace snoop
{
m_stop = true;
if (m_thread.joinable())
{
m_thread.join();
}
}
void ArmDeepSleep(long long startMs, long long stopMs)
{
@@ -106,7 +109,9 @@ namespace snoop
// accept with optional fractional seconds
if (sscanf(s.c_str(), "%d-%d-%dT%d:%d:%d%n", &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec, &n) != 6)
{
return std::nullopt;
}
tm.tm_year -= 1900;
tm.tm_mon -= 1;
const char *p = s.c_str() + n;
@@ -114,14 +119,19 @@ namespace snoop
{
// consume fractional
++p;
while (isdigit(*p))
while (isdigit(*p)) {
++p; // ignore exact fraction
}
}
if (*p != 'Z')
{
return std::nullopt;
}
time_t tt = timegm(&tm);
if (tt < 0)
{
return std::nullopt;
}
return (long long)tt * 1000LL;
}
@@ -183,8 +193,10 @@ namespace snoop
auto sMs = ParseRfc3339UtcToMs(j["start"].get<std::string>());
auto eMs = ParseRfc3339UtcToMs(j["stop"].get<std::string>());
if (sMs && eMs)
{
return *eMs; // caller will use start separately
}
}
if (j.contains("start") && j["start"].is_string() &&
j["start"].get<std::string>() == "now" && j.contains("for_s"))
{
@@ -204,7 +216,6 @@ namespace snoop
{
if (u.scheme == "https")
{
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port, crt.string().c_str(), key.string().c_str(), std::string());
cli->enable_server_certificate_verification(false);
cli->set_ca_cert_path(ca.string().c_str());
@@ -221,13 +232,6 @@ namespace snoop
spdlog::debug("[mTLS debug] SSLClient initialized OK for host={} port={}", u.host, u.port);
}
return cli;
#else
(void)u;
(void)ca;
(void)crt;
(void)key;
throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided");
#endif
}
}
@@ -235,13 +239,19 @@ namespace snoop
void SleepWithJitterOnce(std::mt19937 &rng, int baseSec, int jitterSec)
{
if (baseSec < 0)
{
baseSec = 0;
}
if (jitterSec < 0)
{
jitterSec = 0;
}
std::uniform_int_distribution<int> dist(-jitterSec, +jitterSec);
int delay = baseSec + dist(rng);
if (delay < 0)
{
delay = 0;
}
std::this_thread::sleep_for(std::chrono::seconds(delay));
}
@@ -271,6 +281,8 @@ namespace snoop
return m_handlers.onUpdateConfig ? m_handlers.onUpdateConfig : NotImplemented;
if (type == "set_deep_sleep")
return m_handlers.onSetDeepSleep ? m_handlers.onSetDeepSleep : NotImplemented;
if (type == "renew_cert")
return m_handlers.onRenewCert ? m_handlers.onRenewCert : NotImplemented;
return NotImplemented;
}
@@ -282,9 +294,13 @@ namespace snoop
auto to_id = [](const nlohmann::json &v) -> uint64_t
{
if (v.is_number_unsigned())
{
return v.get<uint64_t>();
}
if (v.is_number_integer())
{
return static_cast<uint64_t>(v.get<long long>());
}
if (v.is_string())
{
try
@@ -302,10 +318,13 @@ namespace snoop
{
Task t;
if (x.contains("id"))
{
t.id = to_id(x["id"]);
}
if (x.contains("type") && x["type"].is_string())
{
t.type = x["type"].get<std::string>();
}
// payload can be stringified JSON or object
try
{
@@ -339,9 +358,13 @@ namespace snoop
}
if (t.id == 0)
{
spdlog::warn("Task without valid id: {}", x.dump());
}
if (t.type.empty())
{
spdlog::warn("Task without type: {}", x.dump());
}
out.push_back(std::move(t));
};
@@ -406,10 +429,14 @@ namespace snoop
spdlog::info("Entering deep sleep until {} ms", stopMs);
// Stop stream & recording immediately
if (m_controls.stopStreamNow)
{
m_controls.stopStreamNow();
}
if (m_controls.stopRecordingNow)
{
m_controls.stopRecordingNow();
}
}
bool EnsureTaskClient()
{
@@ -433,7 +460,6 @@ namespace snoop
return true;
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
try
{
auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring();
@@ -459,12 +485,6 @@ namespace snoop
spdlog::error("EnsureTaskClient: failed to set client cert: {}", e.what());
return false;
}
#else
(void)ca;
(void)crt;
spdlog::error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT not enabled");
return false;
#endif
}
void RunLoop()

View File

@@ -15,6 +15,10 @@
#include <stdio.h>
#include <sys/wait.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/asn1.h>
#include "ConfigService.h"
namespace snoop
@@ -251,6 +255,103 @@ namespace snoop
return true;
}
// Renew cert either because it's about to expire (force=false) or
// because server commanded us (force=true).
// Returns true if renew was done and new files were written.
bool RenewCertificate(bool force = false)
{
const std::string guid = m_cfg->GetGuid();
const std::filesystem::path keystoreDir = "/etc/iot/keys";
const auto certPath = keystoreDir / "device.crt.pem";
const auto caPath = keystoreDir / "issuing_ca.pem";
const auto chainPath = keystoreDir / "ca_chain.pem";
// ----- 1) expiry check (case 1) -----
if (!force)
{
if (!std::filesystem::exists(certPath))
{
spdlog::warn("RenewCertificate: cert not found, skip (not forced)");
return false;
}
if (!IsCertExpiring(certPath, 1))
{
spdlog::info("RenewCertificate: cert is fine, skip");
return false;
}
spdlog::info("RenewCertificate: cert expiring -> will renew");
}
else
{
spdlog::info("RenewCertificate: forced renew");
}
// ----- 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 csrName = "device_" + guid + ".csr";
if (!std::filesystem::exists(csrName))
{
throw std::runtime_error("Renew: CSR was not generated");
}
std::string csrPem = ReadFile(csrName);
// ----- 3) build mTLS client using current device cert + keyring key -----
const std::string base = m_cfg->GetBaseUrl();
auto pu = ParseBaseUrl(base);
auto cli = MakeMtlsClient(pu);
// ----- 4) send multipart to /api/renew/:guid -----
const std::string path = "/api/renew/" + guid;
httplib::MultipartFormDataItems items = {
{"csr", csrPem, "request.csr", "text/plain"}};
spdlog::info("POST {} (multipart, mTLS)", path);
auto res = cli->Post(path.c_str(), items);
if (!res)
throw std::runtime_error("Renew request failed (no response)");
spdlog::info("Renew response status: {}", res->status);
if (res->status != 200)
throw std::runtime_error("Renew failed: HTTP " + std::to_string(res->status));
nlohmann::json j = nlohmann::json::parse(res->body);
if (!j.contains("certificate") || !j["certificate"].is_string())
throw std::runtime_error("Renew response missing or invalid 'certificate'");
WriteFile(certPath, j["certificate"].get<std::string>());
if (j.contains("issuing_ca") && j["issuing_ca"].is_string())
{
WriteFile(caPath, j["issuing_ca"].get<std::string>());
}
if (j.contains("ca_chain"))
{
std::string chainPem;
if (j["ca_chain"].is_string())
chainPem = j["ca_chain"].get<std::string>();
else if (j["ca_chain"].is_array())
chainPem = JoinPemArray(j["ca_chain"]);
else
throw std::runtime_error("'ca_chain' must be string or array");
WriteFile(chainPath, chainPem);
}
if (j.contains("old_serial") && j["old_serial"].is_string())
{
spdlog::info("Renew: server says old_serial={}", j["old_serial"].get<std::string>());
}
spdlog::info("RenewCertificate: success");
return true;
}
private:
// very small helper to pack PEM into JSON string value (single-line, escaped quotes & newlines)
static std::string EscapeJsonForOneLine(const std::string &in)
@@ -298,15 +399,138 @@ namespace snoop
{
const auto &v = arr.at(i);
if (!v.is_string())
{
throw std::runtime_error("ca_chain element is not string");
}
if (!out.empty() && out.back() != '\n')
out.push_back('\n');
out += v.get<std::string>();
if (out.back() != '\n')
{
out.push_back('\n');
}
out += v.get<std::string>();
if (out.back() != '\n')
{
out.push_back('\n');
}
}
return out;
}
static bool IsCertExpiring(const std::filesystem::path &certPath, int daysThreshold)
{
// we will use raw OpenSSL to read the PEM and check notAfter
FILE *f = fopen(certPath.string().c_str(), "r");
if (!f)
{
spdlog::warn("IsCertExpiring: cannot open cert {}", certPath.string());
return false;
}
X509 *cert = PEM_read_X509(f, nullptr, nullptr, nullptr);
fclose(f);
if (!cert)
{
spdlog::warn("IsCertExpiring: cannot parse X509 from {}", certPath.string());
return false;
}
const ASN1_TIME *notAfter = X509_get0_notAfter(cert);
if (!notAfter)
{
spdlog::warn("IsCertExpiring: no notAfter in cert");
X509_free(cert);
return false;
}
// current time
time_t now_t = time(nullptr);
// convert ASN1_TIME → time_t diff
int days = 0, secs = 0;
// OpenSSL has ASN1_TIME_diff in newer versions
if (ASN1_TIME_diff(&days, &secs, nullptr, notAfter) == 0)
{
spdlog::warn("IsCertExpiring: ASN1_TIME_diff failed");
X509_free(cert);
return false;
}
X509_free(cert);
// days can be negative -> already expired
spdlog::info("IsCertExpiring: certificate expires in {} days and {} seconds", days, secs);
if (days < 0)
{
return true; // already expired
}
if (days == 0 && secs <= 0)
{
return true;
}
return days <= daysThreshold;
}
struct ParsedUrl
{
std::string scheme;
std::string host;
int port;
};
static ParsedUrl ParseBaseUrl(const std::string &base)
{
// same simple regex as in DeviceControlService
std::regex re(R"(^\s*(https?)://([^/:]+)(?::(\d+))?\s*$)");
std::smatch m;
if (!std::regex_match(base, m, re))
{
throw std::runtime_error("EnrollmentService: invalid base URL: " + base);
}
ParsedUrl u;
u.scheme = m[1].str();
u.host = m[2].str();
u.port = m[3].matched ? std::stoi(m[3].str()) : (u.scheme == "https" ? 443 : 80);
return u;
}
// create SSL client with: CA from /etc/iot/keys, device cert from disk,
// key from *kernel keyring* (decrypted to a temp file)
static std::unique_ptr<httplib::SSLClient> MakeMtlsClient(const ParsedUrl &u)
{
// 1) choose CA
std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem";
if (!std::filesystem::exists(ca))
{
ca = "/etc/iot/keys/ca_chain.pem";
}
// 2) device cert
const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
if (!std::filesystem::exists(crt))
{
throw std::runtime_error("MakeMtlsClient: device.crt.pem not found");
}
// 3) read key payload from kernel keyring and dump to temp (like DeviceControlService)
auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring();
snoop::device_sec::TempFile tf(std::filesystem::temp_directory_path());
tf.write_all(payload.data(), payload.size());
// 4) build SSL client
auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(),
u.port,
crt.string().c_str(),
tf.path.string().c_str(),
std::string() /*no password*/);
// NOTE: we keep verification relaxed for now, like in your task loop
cli->enable_server_certificate_verification(false);
cli->set_ca_cert_path(ca.string().c_str());
cli->set_connection_timeout(10);
cli->set_read_timeout(60);
cli->set_write_timeout(60);
if (!cli->is_valid())
{
throw std::runtime_error("MakeMtlsClient: SSLClient not valid");
}
return cli;
}
};
} // namespace snoop

View File

@@ -83,6 +83,10 @@ namespace snoop
} catch (const std::exception& e) {
spdlog::error("WHIP POST offer failed: {}", e.what());
} });
// m_pc->onLocalCandidate([this](rtc::Candidate c)
// {
// try { PatchCandidateWHIP(c); }
// catch (const std::exception& e) { spdlog::warn("WHIP PATCH candidate failed: {}", e.what()); } });
m_pc->onLocalCandidate([this](rtc::Candidate c)
{

View File

@@ -42,6 +42,14 @@ namespace snoop
{
spdlog::info("First-run enrollment completed.");
}
try
{
enroll.RenewCertificate(false);
}
catch (const std::exception &e)
{
spdlog::warn("Auto-renew check failed: {}", e.what());
}
}
auto writerService = std::make_shared<AudioWriterService>(configService, "records");
@@ -76,8 +84,10 @@ namespace snoop
try
{
if (t.payload.contains("whipUrl"))
{
whipUrl = t.payload.at("whipUrl").get<std::string>();
}
}
catch (...)
{
}
@@ -156,7 +166,9 @@ namespace snoop
auto s = snoop::DeviceControlService::ParseRfc3339UtcToMs(t.payload["start"].get<std::string>());
auto e = snoop::DeviceControlService::ParseRfc3339UtcToMs(t.payload["stop"].get<std::string>());
if (!s || !e)
{
throw std::runtime_error("Invalid RFC3339 times");
}
startMs = *s;
stopMs = *e;
}
@@ -170,8 +182,9 @@ namespace snoop
throw std::runtime_error("Missing/invalid start/stop time");
}
if (stopMs <= startMs)
{
throw std::runtime_error("stop <= start");
}
g_taskSvc->ArmDeepSleep(startMs, stopMs);
return snoop::DeviceControlService::HandlerResult{
true,
@@ -183,6 +196,23 @@ namespace snoop
return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()};
}
};
handlers.onRenewCert = [enrollSvc](const snoop::DeviceControlService::Task &t)
{
spdlog::info("renew_cert task received, payload: {}", t.payload.dump());
try
{
bool ok = enrollSvc->RenewCertificate(true); // <- force
if (ok)
{
return snoop::DeviceControlService::HandlerResult{true, R"({"renewed":true})", ""};
}
return snoop::DeviceControlService::HandlerResult{true, R"({"renewed":false})", ""};
}
catch (const std::exception &e)
{
return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()};
}
};
}
g_taskSvc = std::make_unique<snoop::DeviceControlService>(configService, handlers, controls);