DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
audit_log.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file audit_log.h
6 * @brief Tamper-evident, hash-chained audit log (DETWS_ENABLE_AUDIT_LOG).
7 *
8 * An append-only security log where each entry carries
9 * `hash = SHA-256(prev_hash || seq || ts || category || msg)`, chaining every
10 * record to its predecessor. Altering, reordering, or deleting any retained
11 * record breaks the chain, which detws_audit_verify() detects in O(n). All
12 * storage is a fixed RAM ring of DETWS_AUDIT_LOG_ENTRIES records (no heap,
13 * bounded latency); when it wraps, the evicted record's hash becomes a moving
14 * chain anchor, so the *retained window* still verifies end-to-end.
15 *
16 * **Durability / forwarding.** The RAM ring is only the recent window for query
17 * and verification. Install a sink with detws_audit_set_sink() to forward every
18 * record, at the moment it is created (before it can ever be evicted), to a
19 * durable or remote store - an SD-card file, a syslog / HTTP log service, a
20 * serial console. Because the sink receives the full record including its chain
21 * hash, the external store preserves the same tamper-evident chain. Use
22 * detws_audit_format() to render a record as one JSON line for that sink.
23 *
24 * Pure and host-tested (the chain is the same on host and ESP32; SHA-256 comes
25 * from ssh_sha256, hardware-accelerated on ESP32). Single-accessor like the log
26 * buffer: append from one context (a worker / loop), not concurrently.
27 *
28 * @author Douglas Quigg (dstroy0)
29 * @date 2026
30 */
31
32#ifndef DETERMINISTICESPASYNCWEBSERVER_AUDIT_LOG_H
33#define DETERMINISTICESPASYNCWEBSERVER_AUDIT_LOG_H
34
35#include "ServerConfig.h"
36#include <stddef.h>
37#include <stdint.h>
38
39#if DETWS_ENABLE_AUDIT_LOG
40
41/** @brief SHA-256 chain-hash length per record. */
42#define DETWS_AUDIT_HASH_LEN 32
43
44/** @brief Standard audit event categories (extend with your own values). */
45enum class DetwsAuditCat : uint8_t
46{
47 DETWS_AUDIT_SYSTEM = 0, ///< Boot, shutdown, time change, generic.
48 DETWS_AUDIT_AUTH = 1, ///< Authentication success (login).
49 DETWS_AUDIT_AUTH_FAIL = 2, ///< Authentication failure.
50 DETWS_AUDIT_ACCESS = 3, ///< Resource access (request served / denied).
51 DETWS_AUDIT_CONFIG = 4, ///< Configuration change.
52 DETWS_AUDIT_ADMIN = 5, ///< Privileged / administrative action.
53};
54
55/** @brief One audit record. seq is monotonic and never reused across evictions. */
56struct DetwsAuditEntry
57{
58 uint32_t seq; ///< Monotonic sequence number (1-based).
59 uint32_t ts; ///< Timestamp from detws_millis() at append.
60 DetwsAuditCat category; ///< audit category (a ::DetwsAuditCat, or a user value cast in).
61 char msg[DETWS_AUDIT_MSG_LEN]; ///< Null-terminated message (truncated).
62 uint8_t hash[DETWS_AUDIT_HASH_LEN]; ///< SHA-256(prev_hash || fields).
63};
64
65/** @brief Sink invoked once per record, at append time, for durable forwarding. */
66typedef void (*detws_audit_sink_fn)(const DetwsAuditEntry *entry);
67
68/** @brief Clear the ring, reset the sequence counter and the chain anchor to genesis. */
69void detws_audit_reset(void);
70
71/**
72 * @brief Forward every future record to @p sink at append time (nullptr to detach).
73 *
74 * The sink runs synchronously inside detws_audit_append(); keep it short and
75 * non-reentrant (do not call detws_audit_append() from it).
76 */
77void detws_audit_set_sink(detws_audit_sink_fn sink);
78
79/**
80 * @brief Append a record and return its sequence number.
81 *
82 * @param category ::DetwsAuditCat or a user-defined value.
83 * @param msg Null-terminated message (truncated to DETWS_AUDIT_MSG_LEN-1).
84 * @return the assigned monotonic sequence number.
85 */
86uint32_t detws_audit_append(DetwsAuditCat category, const char *msg);
87
88/** @brief Number of records currently retained in the ring (0 .. DETWS_AUDIT_LOG_ENTRIES). */
89uint16_t detws_audit_count(void);
90
91/** @brief Record @p i (0 = oldest retained .. count-1 = newest), or nullptr if out of range. */
92const DetwsAuditEntry *detws_audit_at(uint16_t i);
93
94/**
95 * @brief Recompute the chain over the retained window and report integrity.
96 *
97 * @param first_broken_seq if non-null, set to the seq of the first record whose
98 * hash does not match when the chain is broken.
99 * @return true if every retained record verifies against its predecessor.
100 */
101bool detws_audit_verify(uint32_t *first_broken_seq);
102
103/** @brief Human-readable name for a standard ::DetwsAuditCat ("system" for unknown). */
104const char *detws_audit_cat_name(DetwsAuditCat category);
105
106/**
107 * @brief Render one record as a JSON object (hash as full 64-char hex).
108 * @return characters written (excluding NUL), or 0 if @p cap is too small.
109 */
110int detws_audit_format(const DetwsAuditEntry *entry, char *out, size_t cap);
111
112/**
113 * @brief Dump the retained window as a JSON document for an endpoint.
114 *
115 * `{"intact":bool,"count":N,"entries":[ {record}, ... ]}` (plus "first_broken"
116 * when the chain is broken).
117 *
118 * @return characters written (excluding NUL), or 0 if @p cap is too small.
119 */
120int detws_audit_dump_json(char *out, size_t cap);
121
122#endif // DETWS_ENABLE_AUDIT_LOG
123#endif // DETERMINISTICESPASYNCWEBSERVER_AUDIT_LOG_H
User-facing configuration for DeterministicESPAsyncWebServer.