Compare commits
3 Commits
dev
...
dev_openss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49482ea4c | ||
|
|
fe4affd942 | ||
|
|
21088468b7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@
|
|||||||
/toolchain/build/
|
/toolchain/build/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
check/
|
||||||
@@ -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
512
src/Security/SslCertUtil.h
Normal 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
|
||||||
|
// (you’ll still call `shred -u` from EnrollmentService).
|
||||||
|
static void EncryptFileAes256CbcPbkdf2(const std::filesystem::path &inPath,
|
||||||
|
const std::filesystem::path &outPath,
|
||||||
|
const std::string &password)
|
||||||
|
{
|
||||||
|
spdlog::info("SslCertUtil: encrypting '{}' -> '{}' (AES-256-CBC + PBKDF2)",
|
||||||
|
inPath.string(), outPath.string());
|
||||||
|
|
||||||
|
// ----- 1) Read input file -----
|
||||||
|
std::ifstream fin(inPath, std::ios::binary);
|
||||||
|
if (!fin)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: cannot open input file: " +
|
||||||
|
inPath.string());
|
||||||
|
}
|
||||||
|
std::vector<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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,9 +107,11 @@ 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");
|
||||||
return fallback;
|
return 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();
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -101,7 +103,8 @@ namespace snoop
|
|||||||
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))
|
||||||
|
|||||||
35
src/main.cpp
35
src/main.cpp
@@ -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,21 +36,39 @@ 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);
|
||||||
EnrollmentService enroll(configService);
|
// const bool didEnroll = enroll.EnsureEnrolled();
|
||||||
const bool didEnroll = enroll.EnsureEnrolled();
|
// if (didEnroll)
|
||||||
if (didEnroll)
|
// {
|
||||||
{
|
// spdlog::info("First-run enrollment completed.");
|
||||||
spdlog::info("First-run enrollment completed.");
|
// }
|
||||||
}
|
auto enrollSvc = std::make_shared<EnrollmentService>(configService);
|
||||||
|
enrollSvc->EnsureEnrolled();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
enroll.RenewCertificate(false);
|
enrollSvc->RenewCertificate(false);
|
||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
spdlog::warn("Auto-renew check failed: {}", e.what());
|
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("Main: client key loaded into kernel keyring for this session");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
spdlog::info("Main: client key already present in kernel keyring");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
spdlog::warn("Main: failed to ensure client key in keyring: {}", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto writerService = std::make_shared<AudioWriterService>(configService, "records");
|
auto writerService = std::make_shared<AudioWriterService>(configService, "records");
|
||||||
|
|||||||
Reference in New Issue
Block a user