HMAC File Integrity Checker (cpp, written by Codex)
envgap__codex__cpp-t1-13
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
mbedtls 3.6.0 ZIP missing framework submodule
Not a benchmark task.
- It was made to work, but its repair cannot be rebuilt from the saved files (the saved copy shows no change, or not all of the changes the study's notes describe), so there is no fix to score against.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/cpp-t1 #13 · read the task the agent was given
Codex wrote this cpp project from the task below. It does not run on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: HMAC File Integrity Checker Write a program that computes and verifies HMAC-based integrity checksums for files and directories, creating a manifest of checksums that can later be used to detect unauthorized modifications. FUNCTIONAL REQUIREMENTS: - Accept a file or directory path, a secret key, and a mode (generate or verify) as command-line arguments - Support multiple hash algorithms selectable via --algorithm flag: SHA-256 (default), SHA-384, SHA-512, SHA3-256 - generate mode: Compute HMAC for each file and save a manifest file containing file paths, HMAC values, file sizes, and timestamps - verify mode: Read the manifest, recompute HMACs, and report which files are unchanged, modified, added (present but not in manifest), or missing (in manifest but deleted) - For directories, recursively process all files and support --exclude flag with glob patterns to skip files (e.g., --exclude "*.log,*.tmp") - Support a --output flag for the manifest file path (default: integrity_manifest.json) - Print a colored summary to console showing verification results: passed files in green, modified in red, missing in yellow, new files in blue - Compute a master HMAC over the entire manifest to detect tampering of the manifest file itself - Support incremental updates: when generating with an existing manifest, only recompute HMACs for files whose modification time has changed via --incremental flag - If no arguments are given, generate sample files in a temporary directory, create an integrity manifest, modify one file, delete another, add a new file, then run verification to demonstrate all detection capabilities - Handle binary and text files correctly, permission errors gracefully Create a complete C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include: - Source code - CMakeLists.txt with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(hmac_file_integrity_checker_cpp VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
include(FetchContent)
# Pinned mbedTLS
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
mbedtls
URL https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v3.6.0.zip
)
# Pinned nlohmann/json
FetchContent_Declare(
nlohmann_json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(mbedtls nlohmann_json)
add_executable(hmac_checker src/main.cpp)
target_link_libraries(hmac_checker PRIVATE mbedtls mbedx509 mbedcrypto nlohmann_json::nlohmann_json)
target_include_directories(hmac_checker PRIVATE ${mbedtls_SOURCE_DIR}/include)
README.md
# HMAC File Integrity Checker (C++) Computes and verifies HMAC manifests for files/directories to detect unauthorized changes. ## Requirements - Ubuntu 22.04 - G++ 12+ - CMake 3.22+ ## Dependencies - Direct (pinned in `CMakeLists.txt`): - `mbedTLS v3.6.0` - `nlohmann/json v3.11.3` - Transitive: - `mbedcrypto`, `mbedx509` (from pinned mbedTLS source) ## Build ```bash cmake -S . -B build cmake --build build --config Release ``` ## Run ```bash ./build/hmac_checker <generate|verify> <path> <secret-key> [--algorithm SHA-256|SHA-384|SHA-512|SHA3-256] [--output integrity_manifest.json] [--exclude "*.log,*.tmp"] [--incremental] ``` Examples: ```bash ./build/hmac_checker generate ./data my-secret --algorithm SHA-512 --output ./integrity_manifest.json ./build/hmac_checker verify ./data my-secret --output ./integrity_manifest.json ./build/hmac_checker generate ./data my-secret --exclude "*.log,*.tmp" --incremental ``` ## Behavior - `generate` creates a JSON manifest with file path, HMAC, file size, mtime, and timestamp. - Computes a `masterHmac` over canonical manifest content to detect manifest tampering. - `verify` reports `UNCHANGED`, `MODIFIED`, `MISSING`, and `ADDED` in color. - Supports recursive directories, glob excludes, incremental generation, and no-argument demo mode.
src/main.cpp
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <regex>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
#include <mbedtls/error.h>
#include <mbedtls/md.h>
#include <nlohmann/json.hpp>
namespace {
constexpr const char* C_RESET = "\033[0m";
constexpr const char* C_GREEN = "\033[32m";
constexpr const char* C_RED = "\033[31m";
constexpr const char* C_YELLOW = "\033[33m";
constexpr const char* C_BLUE = "\033[34m";
struct ParsedArgs {
std::map<std::string, std::string> options;
std::vector<std::string> positional;
};
struct AlgoSpec {
std::string label;
mbedtls_md_type_t mdType;
};
struct Entry {
std::string path;
std::string hmac;
std::uintmax_t size {};
long long mtimeMs {};
std::string timestamp;
};
struct Collection {
std::vector<std::filesystem::path> files;
bool rootIsDir {};
};
ParsedArgs parseArgs(int argc, char** argv) {
ParsedArgs out;
for (int i = 1; i < argc; i++) {
std::string t = argv[i];
if (t.rfind("--", 0) == 0) {
std::string k = t.substr(2);
if (i + 1 < argc && std::string(argv[i + 1]).rfind("--", 0) != 0) out.options[k] = argv[++i];
else out.options[k] = "true";
} else {
out.positional.push_back(t);
}
}
return out;
}
[[noreturn]] void throwMbed(int rc, const std::string& where) {
char buf[256];
mbedtls_strerror(rc, buf, sizeof(buf));
throw std::runtime_error(where + ": " + std::string(buf));
}
std::string portable(const std::filesystem::path& p) {
std::string s = p.string();
std::replace(s.begin(), s.end(), '\\', '/');
return s;
}
AlgoSpec normalizeAlgo(const std::string& raw) {
std::string v = raw;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (v == "sha-256" || v == "sha256") return {"sha-256", MBEDTLS_MD_SHA256};
if (v == "sha-384" || v == "sha384") return {"sha-384", MBEDTLS_MD_SHA384};
if (v == "sha-512" || v == "sha512") return {"sha-512", MBEDTLS_MD_SHA512};
if (v == "sha3-256") {
#ifdef MBEDTLS_MD_SHA3_256
return {"sha3-256", MBEDTLS_MD_SHA3_256};
#else
throw std::runtime_error("SHA3-256 is unavailable in this mbedTLS build.");
#endif
}
throw std::runtime_error("Unsupported algorithm. Use SHA-256, SHA-384, SHA-512, or SHA3-256.");
}
std::regex globToRegex(const std::string& glob) {
std::string out = "^";
for (char c : glob) {
if (c == '*') out += ".*";
else if (c == '?') out += ".";
else if (std::string(R"(\.^$|()[]{}+)").find(c) != std::string::npos) out += std::string("\\") + c;
else out += c;
}
out += "$";
return std::regex(out, std::regex::icase);
}
std::vector<std::regex> parseExcludes(const std::string& raw) {
std::vector<std::regex> out;
std::stringstream ss(raw);
std::string item;
while (std::getline(ss, item, ',')) {
const auto a = item.find_first_not_of(" \t\r\n");
if (a == std::string::npos) continue;
const auto b = item.find_last_not_of(" \t\r\n");
out.push_back(globToRegex(item.substr(a, b - a + 1)));
}
return out;
}
bool excluded(const std::string& rel, const std::string& name, const std::vector<std::regex>& excludes) {
for (const auto& r : excludes) {
if (std::regex_match(rel, r) || std::regex_match(name, r)) return true;
}
return false;
}
Collection collectFiles(const std::filesystem::path& target, const std::vector<std::regex>& excludes) {
if (std::filesystem::is_regular_file(target)) return {{target}, false};
if (!std::filesystem::is_directory(target)) throw std::runtime_error("Target path must be file or directory.");
std::vector<std::filesystem::path> files;
std::error_code ec;
std::filesystem::recursive_directory_iterator it(
target,
std::filesystem::directory_options::skip_permission_denied,
ec
);
for (std::filesystem::recursive_directory_iterator end; it != end; it.increment(ec)) {
if (ec) {
ec.clear();
continue;
}
if (!it->is_regular_file(ec)) {
ec.clear();
continue;
}
const std::string rel = portable(std::filesystem::relative(it->path(), target, ec));
ec.clear();
if (excluded(rel, it->path().filename().string(), excludes)) continue;
files.push_back(it->path());
}
std::sort(files.begin(), files.end());
return {files, true};
}
std::string relKey(const std::filesystem::path& root, const std::filesystem::path& file, bool rootIsDir) {
return rootIsDir ? portable(std::filesystem::relative(file, root)) : file.filename().string();
}
std::vector<unsigned char> readFile(const std::filesystem::path& p) {
std::ifstream in(p, std::ios::binary);
if (!in.is_open()) throw std::runtime_error("Failed to read file: " + p.string());
return std::vector<unsigned char>(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
std::string toHex(const std::vector<unsigned char>& b) {
std::ostringstream out;
for (unsigned char c : b) out << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c);
return out.str();
}
std::vector<unsigned char> hmacRaw(const std::vector<unsigned char>& data, const std::string& secret, mbedtls_md_type_t type) {
const mbedtls_md_info_t* md = mbedtls_md_info_from_type(type);
if (!md) throw std::runtime_error("Digest implementation not available.");
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
int rc = mbedtls_md_setup(&ctx, md, 1);
if (rc != 0) {
mbedtls_md_free(&ctx);
throwMbed(rc, "mbedtls_md_setup");
}
rc = mbedtls_md_hmac_starts(&ctx, reinterpret_cast<const unsigned char*>(secret.data()), secret.size());
if (rc != 0) {
mbedtls_md_free(&ctx);
throwMbed(rc, "mbedtls_md_hmac_starts");
}
rc = mbedtls_md_hmac_update(&ctx, data.data(), data.size());
if (rc != 0) {
mbedtls_md_free(&ctx);
throwMbed(rc, "mbedtls_md_hmac_update");
}
std::vector<unsigned char> out(mbedtls_md_get_size(md));
rc = mbedtls_md_hmac_finish(&ctx, out.data());
mbedtls_md_free(&ctx);
if (rc != 0) throwMbed(rc, "mbedtls_md_hmac_finish");
return out;
}
std::string hmacFileHex(const std::filesystem::path& p, const std::string& secret, mbedtls_md_type_t type) {
return toHex(hmacRaw(readFile(p), secret, type));
}
long long fileMtimeMs(const std::filesystem::path& p) {
auto ftime = std::filesystem::last_write_time(p);
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()
);
return std::chrono::duration_cast<std::chrono::milliseconds>(sctp.time_since_epoch()).count();
}
std::string isoFromMs(long long ms) {
std::time_t t = static_cast<std::time_t>(ms / 1000);
std::tm tm {};
#if defined(_WIN32)
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
int frac = static_cast<int>(ms % 1000);
std::ostringstream out;
out << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S") << "." << std::setw(3) << std::setfill('0') << frac << "Z";
return out.str();
}
std::string nowIso() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
return isoFromMs(ms);
}
Entry toEntry(const nlohmann::json& j) {
Entry e;
e.path = j.value("path", "");
e.hmac = j.value("hmac", "");
e.size = j.value("size", 0);
e.mtimeMs = j.value("mtimeMs", 0LL);
e.timestamp = j.value("timestamp", "");
return e;
}
nlohmann::json toJson(const Entry& e) {
return {{"path", e.path}, {"hmac", e.hmac}, {"size", e.size}, {"mtimeMs", e.mtimeMs}, {"timestamp", e.timestamp}};
}
std::string masterPayload(const nlohmann::json& manifestNoMaster) {
std::vector<Entry> entries;
for (const auto& x : manifestNoMaster.at("entries")) entries.push_back(toEntry(x));
std::sort(entries.begin(), entries.end(), [](const Entry& a, const Entry& b) { return a.path < b.path; });
std::ostringstream out;
out << manifestNoMaster.value("version", 1) << "\n";
out << manifestNoMaster.value("algorithm", "sha-256") << "\n";
out << manifestNoMaster.value("rootPath", "") << "\n";
out << manifestNoMaster.value("generatedAt", "") << "\n";
for (const auto& e : entries) out << e.path << "|" << e.size << "|" << e.mtimeMs << "|" << e.timestamp << "|" << e.hmac << "\n";
return out.str();
}
std::string masterHmac(const nlohmann::json& manifestNoMaster, const std::string& secret, mbedtls_md_type_t type) {
const std::string payload = masterPayload(manifestNoMaster);
return toHex(hmacRaw(std::vector<unsigned char>(payload.begin(), payload.end()), secret, type));
}
void writeManifest(const std::filesystem::path& p, const nlohmann::json& j) {
if (!p.parent_path().empty()) std::filesystem::create_directories(p.parent_path());
std::ofstream out(p);
if (!out.is_open()) throw std::runtime_error("Failed to write manifest: " + p.string());
out << j.dump(2) << "\n";
}
nlohmann::json readManifest(const std::filesystem::path& p) {
std::ifstream in(p);
if (!in.is_open()) throw std::runtime_error("Failed to read manifest: " + p.string());
nlohmann::json j;
in >> j;
return j;
}
void generateMode(
const std::filesystem::path& target,
const std::string& secret,
const std::string& algorithmInput,
const std::filesystem::path& output,
const std::vector<std::regex>& excludes,
bool incremental
) {
const AlgoSpec algo = normalizeAlgo(algorithmInput);
const Collection c = collectFiles(target, excludes);
std::unordered_map<std::string, Entry> previous;
if (incremental && std::filesystem::exists(output)) {
const auto prior = readManifest(output);
if (prior.contains("entries") && prior["entries"].is_array()) {
for (const auto& e : prior["entries"]) {
Entry pe = toEntry(e);
previous[pe.path] = pe;
}
}
}
std::vector<Entry> entries;
for (const auto& f : c.files) {
Entry e;
e.path = relKey(target, f, c.rootIsDir);
e.size = std::filesystem::file_size(f);
e.mtimeMs = fileMtimeMs(f);
e.timestamp = isoFromMs(e.mtimeMs);
auto it = previous.find(e.path);
if (it != previous.end() && it->second.size == e.size && it->second.mtimeMs == e.mtimeMs) e.hmac = it->second.hmac;
else e.hmac = hmacFileHex(f, secret, algo.mdType);
entries.push_back(e);
}
std::sort(entries.begin(), entries.end(), [](const Entry& a, const Entry& b) { return a.path < b.path; });
nlohmann::json manifestNoMaster{
{"version", 1},
{"generatedAt", nowIso()},
{"rootPath", std::filesystem::absolute(target).string()},
{"algorithm", algo.label},
{"entries", nlohmann::json::array()}
};
for (const auto& e : entries) manifestNoMaster["entries"].push_back(toJson(e));
nlohmann::json manifest = manifestNoMaster;
manifest["masterHmac"] = masterHmac(manifestNoMaster, secret, algo.mdType);
writeManifest(output, manifest);
std::cout << "Manifest written: " << output << "\n";
std::cout << "Processed files: " << entries.size() << "\n";
std::cout << "Algorithm: " << algo.label << "\n";
std::cout << "Incremental: " << (incremental ? "enabled" : "disabled") << "\n";
}
bool verifyMode(
const std::filesystem::path& target,
const std::string& secret,
const std::string& algorithmInput,
const std::filesystem::path& output,
const std::vector<std::regex>& excludes
) {
if (!std::filesystem::exists(output)) throw std::runtime_error("Manifest not found: " + output.string());
const auto manifest = readManifest(output);
if (!manifest.contains("entries") || !manifest["entries"].is_array()) throw std::runtime_error("Manifest malformed: entries missing.");
const AlgoSpec manifestAlgo = normalizeAlgo(manifest.value("algorithm", "sha-256"));
const AlgoSpec effectiveAlgo = normalizeAlgo(algorithmInput);
nlohmann::json manifestNoMaster{
{"version", manifest.value("version", 1)},
{"generatedAt", manifest.value("generatedAt", "")},
{"rootPath", manifest.value("rootPath", "")},
{"algorithm", manifestAlgo.label},
{"entries", manifest["entries"]}
};
const bool masterValid = masterHmac(manifestNoMaster, secret, manifestAlgo.mdType) == manifest.value("masterHmac", "");
const Collection c = collectFiles(target, excludes);
std::unordered_map<std::string, std::filesystem::path> current;
for (const auto& f : c.files) current[relKey(target, f, c.rootIsDir)] = f;
std::vector<std::string> unchanged;
std::vector<std::string> modified;
std::vector<std::string> missing;
std::set<std::string> seen;
for (const auto& je : manifest["entries"]) {
const Entry e = toEntry(je);
auto it = current.find(e.path);
if (it == current.end()) {
missing.push_back(e.path);
continue;
}
seen.insert(e.path);
const std::string digest = hmacFileHex(it->second, secret, effectiveAlgo.mdType);
if (digest == e.hmac) unchanged.push_back(e.path);
else modified.push_back(e.path);
}
std::vector<std::string> added;
for (const auto& [k, _] : current) if (seen.count(k) == 0) added.push_back(k);
std::sort(unchanged.begin(), unchanged.end());
std::sort(modified.begin(), modified.end());
std::sort(missing.begin(), missing.end());
std::sort(added.begin(), added.end());
if (!masterValid) std::cout << C_RED << "Manifest master HMAC mismatch: manifest may be tampered." << C_RESET << "\n";
if (effectiveAlgo.label != manifestAlgo.label) {
std::cout << C_YELLOW << "Using --algorithm " << effectiveAlgo.label
<< " instead of manifest algorithm " << manifestAlgo.label << "." << C_RESET << "\n";
}
for (const auto& p : unchanged) std::cout << C_GREEN << "UNCHANGED " << p << C_RESET << "\n";
for (const auto& p : modified) std::cout << C_RED << "MODIFIED " << p << C_RESET << "\n";
for (const auto& p : missing) std::cout << C_YELLOW << "MISSING " << p << C_RESET << "\n";
for (const auto& p : added) std::cout << C_BLUE << "ADDED " << p << C_RESET << "\n";
std::cout << "\nSummary\n";
std::cout << " unchanged: " << unchanged.size() << "\n";
std::cout << " modified : " << modified.size() << "\n";
std::cout << " missing : " << missing.size() << "\n";
std::cout << " added : " << added.size() << "\n";
std::cout << " manifest : " << (masterValid ? "valid" : "tampered") << "\n";
return masterValid && modified.empty() && missing.empty() && added.empty();
}
void demo() {
const auto root = std::filesystem::temp_directory_path() /
("hmac-integrity-" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()));
std::filesystem::create_directories(root / "nested");
std::ofstream(root / "alpha.txt") << "alpha\n";
std::ofstream(root / "beta.txt") << "beta\n";
std::ofstream(root / "nested" / "gamma.bin", std::ios::binary) << "binary-data";
const std::string secret = "demo-secret";
const auto manifest = root / "integrity_manifest.json";
std::cout << "Demo directory: " << root << "\n";
generateMode(root, secret, "sha-256", manifest, {}, false);
std::ofstream(root / "alpha.txt", std::ios::app) << "tampered\n";
std::filesystem::remove(root / "beta.txt");
std::ofstream(root / "new.txt") << "new file\n";
const bool ok = verifyMode(root, secret, "sha-256", manifest, {});
std::cout << "Demo verification result: " << (ok ? "PASS" : "FAIL (expected for demo)") << "\n";
}
std::string usage() {
return "Usage:\n"
" ./hmac_checker <generate|verify> <path> <secret-key> "
"[--algorithm SHA-256|SHA-384|SHA-512|SHA3-256] "
"[--output integrity_manifest.json] [--exclude \"*.log,*.tmp\"] [--incremental]\n\n"
"No arguments runs a demo.";
}
} // namespace
int main(int argc, char** argv) {
try {
ParsedArgs args = parseArgs(argc, argv);
if (args.positional.empty()) {
demo();
return 0;
}
if (args.positional.size() < 3) throw std::runtime_error(usage());
const std::string mode = args.positional[0];
const auto target = std::filesystem::absolute(args.positional[1]);
const std::string secret = args.positional[2];
const std::string algo = args.options.count("algorithm") ? args.options.at("algorithm") : "sha-256";
const auto output = std::filesystem::absolute(args.options.count("output") ? args.options.at("output") : "integrity_manifest.json");
const auto excludes = parseExcludes(args.options.count("exclude") ? args.options.at("exclude") : "");
const bool incremental = args.options.count("incremental") > 0;
if (mode == "generate") {
generateMode(target, secret, algo, output, excludes, incremental);
return 0;
}
if (mode == "verify") {
const bool ok = verifyMode(target, secret, algo, output, excludes);
return ok ? 0 : 2;
}
throw std::runtime_error("Unknown mode. Use generate or verify.");
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
}