ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
AuditLog - a tamper-evident, hash-chained audit log

Layer: L7 Application ยท Build flags: PC_ENABLE_AUDIT_LOG

What this example teaches

Security-relevant events (logins, config changes) need a log you can trust after the fact. This records them in an append-only log where each record chains SHA-256(prev_hash || fields) - so altering or removing any retained record breaks the chain, and /audit reports the break via "intact":false plus the first broken sequence number. A sink forwards every record to durable storage at the moment it is created, before the RAM ring can evict it.

Reset the chain, register a sink, append events.

pc_audit_reset();
pc_audit_set_sink(audit_sink); // durable forwarding, runs per record
pc_audit_append(pc_audit_cat::PC_AUDIT_SYSTEM, "boot");

Events are appended with a category and a message; the library computes the chain hash:

bool ok = pass && strcmp(pass, "secret") == 0;
pc_audit_append(ok ? pc_audit_cat::PC_AUDIT_AUTH : pc_audit_cat::PC_AUDIT_AUTH_FAIL, msg);

The sink is what makes it durable. It runs once per record at append time and receives the full record including its chain hash, so the external copy keeps the same tamper-evident chain even after the in-RAM ring rolls over:

static void audit_sink(const pc_audit_entry *e) {
char line[256];
if (pc_audit_format(e, line, sizeof(line)) > 0) {
Serial.print("[AUDIT] "); Serial.println(line);
// SD card: File f = SD.open("/audit.log", FILE_APPEND); f.println(line); f.close();
// Log svc: pc_webhook_post("http://logs.example/ingest", line);
}
}

pc_audit_dump_json() serializes the chain plus the integrity status for the /audit endpoint.

Build and run

pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPC_ENABLE_AUDIT_LOG=1" \
--lib="." examples/L7-Application/AuditLog/AuditLog.ino
curl "http://<ip>/login?user=alice&pass=secret" # logs auth
curl "http://<ip>/config?http_port=8080" # logs a config change
curl http://<ip>/audit # chain dump + {"intact":true}

Annotated source

The complete sketch (AuditLog.ino), reproduced verbatim with added explanatory comments:

// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
#define PC_ENABLE_AUDIT_LOG 1
#include "protocore.h"
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
PC server;
// Durable forwarding: runs once per record at append time. Point it wherever you
// keep authoritative logs.
static void audit_sink(const pc_audit_entry *e)
{
char line[256];
if (pc_audit_format(e, line, sizeof(line)) > 0)
{
Serial.print("[AUDIT] ");
Serial.println(line);
// SD card: File f = SD.open("/audit.log", FILE_APPEND); f.println(line); f.close();
// Log svc: pc_webhook_post("http://logs.example/ingest", line); // PC_ENABLE_WEBHOOK
}
}
void setup()
{
Serial.begin(115200);
init_wifi_physical(SSID, PASSWORD);
while (!wifi_ready())
delay(250);
Serial.print("IP: ");
uint32_t ip = pc_net_egress_ip(); // library egress IP (network byte order), no Arduino WiFi
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
pc_audit_reset();
pc_audit_set_sink(audit_sink);
pc_audit_append(pc_audit_cat::PC_AUDIT_SYSTEM, "boot");
server.on("/login", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *req) {
const char *user = http_get_query(req, "user");
const char *pass = http_get_query(req, "pass");
char msg[PC_AUDIT_MSG_LEN];
bool ok = pass && strcmp(pass, "secret") == 0;
snprintf(msg, sizeof(msg), "login %s", user ? user : "?");
pc_audit_append(ok ? pc_audit_cat::PC_AUDIT_AUTH : pc_audit_cat::PC_AUDIT_AUTH_FAIL, msg);
server.send(id, ok ? 200 : 401, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false}");
});
server.on("/config", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *req) {
const char *port = http_get_query(req, "http_port");
char msg[PC_AUDIT_MSG_LEN];
snprintf(msg, sizeof(msg), "set http_port=%s", port ? port : "?");
pc_audit_append(pc_audit_cat::PC_AUDIT_CONFIG, msg);
server.send(id, 200, "application/json", "{\"ok\":true}");
});
server.on("/audit", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *) {
char doc[2048];
if (pc_audit_dump_json(doc, sizeof(doc)) > 0)
server.send(id, 200, "application/json", doc);
else
server.send(id, 500, "application/json", "{\"error\":\"buffer\"}");
});
server.begin(80);
}
void loop()
{
server.handle();
}
Tamper-evident, hash-chained audit log (PC_ENABLE_AUDIT_LOG).
Single-port HTTP server with deterministic, zero-allocation execution.
Definition protocore.h:348
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void handle()
Drive the server - call every Arduino loop() iteration.
const char * http_get_query(const HttpReq *req, const char *key)
Look up a query parameter value by name (case-sensitive).
bool init_wifi_physical(const char *, const char *)
Connect to a WiFi access point.
Definition physical.cpp:41
uint32_t pc_net_egress_ip(void)
IPv4 (network byte order) of the current egress interface, or 0 if none.
Definition physical.cpp:77
bool wifi_ready()
True if the WiFi station link is up (associated + an IP is assigned).
Definition physical.cpp:45
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
Layer 7 (Application) - public HTTP routing API.
@ HTTP_GET
Safe, idempotent read.
Fully-parsed HTTP/1.1 request.