From 5b27bb0950fd03d29c223b5960faff89baf37970 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 09:20:39 +0200 Subject: audit: verify voucher chains and attachment hashes --- src/commands.c | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 10 deletions(-) (limited to 'src/commands.c') diff --git a/src/commands.c b/src/commands.c index 448d03e..d21d4c8 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1545,25 +1545,119 @@ static yyjson_mut_val *h_audit_list(struct req *r) return out; } +static int verify_attachments(sqlite3 *db, int64_t *checked, int64_t *bad_id, + char **err) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(db, + "SELECT id,sha256,size_bytes,content FROM attachments" + " ORDER BY id", + -1, &st, NULL) != SQLITE_OK) { + if (err && !*err) + *err = xstrdup(sqlite3_errmsg(db)); + return -1; + } + int ret = 0; + for (;;) { + int step = sqlite3_step(st); + if (step == SQLITE_DONE) + break; + if (step != SQLITE_ROW) { + if (err && !*err) + *err = xstrdup(sqlite3_errmsg(db)); + ret = -1; + break; + } + int64_t id = sqlite3_column_int64(st, 0); + const void *sha = sqlite3_column_blob(st, 1); + int sha_n = sqlite3_column_bytes(st, 1); + int64_t size = sqlite3_column_int64(st, 2); + const void *content = sqlite3_column_blob(st, 3); + int content_n = sqlite3_column_bytes(st, 3); + unsigned char digest[32]; + util_sha256(content, (size_t)content_n, digest); + (*checked)++; + if (sha_n != 32 || memcmp(sha, digest, 32) != 0 || + size != content_n) { + if (!*bad_id) + *bad_id = id; + ret = 1; + } + } + sqlite3_finalize(st); + return ret; +} + static yyjson_mut_val *h_audit_verify(struct req *r) { - int64_t checked = 0, bad_seq = 0; + int full = 0; + arg_bool(r->args, "full", &full); + + int64_t audit_checked = 0, audit_bad = 0; char *err = NULL; - int rc = audit_verify(r->db, &checked, &bad_seq, &err); - if (rc < 0) { + int audit_rc = audit_verify(r->db, &audit_checked, &audit_bad, &err); + if (audit_rc < 0) { + yyjson_mut_val *res = fail(r, "INTERNAL", err ? err : "verify failed"); + free(err); + return res; + } + free(err); + err = NULL; + + struct ledger_verify_result lv; + int ledger_rc = ledger_verify(r->db, &lv, &err); + if (ledger_rc < 0) { yyjson_mut_val *res = fail(r, "INTERNAL", err ? err : "verify failed"); free(err); return res; } free(err); + err = NULL; + + int64_t att_checked = 0, att_bad = 0; + int att_rc = 0; + if (full) { + att_rc = verify_attachments(r->db, &att_checked, &att_bad, &err); + if (att_rc < 0) { + yyjson_mut_val *res = + fail(r, "INTERNAL", err ? err : "verify failed"); + free(err); + return res; + } + free(err); + } + + int ok = audit_rc == 0 && ledger_rc == 0 && att_rc == 0; yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); - yyjson_mut_obj_add_bool(r->rdoc, o, "ok", rc == 0); - yyjson_mut_obj_add_strcpy(r->rdoc, o, "kind", "audit"); - yyjson_mut_obj_add_int(r->rdoc, o, "checked", checked); - if (rc == 1) - yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_seq", bad_seq); + yyjson_mut_obj_add_bool(r->rdoc, o, "ok", ok); + yyjson_mut_obj_add_int(r->rdoc, o, "checked", audit_checked); + yyjson_mut_obj_add_int(r->rdoc, o, "vouchers_checked", + lv.vouchers_checked); + yyjson_mut_obj_add_int(r->rdoc, o, "unbalanced_vouchers", + lv.unbalanced_vouchers); + if (audit_bad) + yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_seq", audit_bad); else yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_seq"); + if (lv.first_bad_voucher_id) + yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_voucher_id", + lv.first_bad_voucher_id); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_voucher_id"); + if (lv.first_unbalanced_voucher_id) + yyjson_mut_obj_add_int(r->rdoc, o, "first_unbalanced_voucher_id", + lv.first_unbalanced_voucher_id); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_unbalanced_voucher_id"); + if (full) { + yyjson_mut_obj_add_int(r->rdoc, o, "attachments_checked", + att_checked); + if (att_bad) + yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_attachment_id", + att_bad); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_attachment_id"); + } return o; } @@ -4438,8 +4532,8 @@ const struct command g_commands[] = { h_agent_instructions }, { "audit.list", "Read the audit log for the active org", PERM_READ, 1, 0, 0, h_audit_list }, - { "audit.verify", "Verify the audit hash chain", PERM_READ, 0, 0, 0, - h_audit_verify }, + { "audit.verify", "Verify the voucher and audit hash chains", PERM_READ, 0, + 0, 0, h_audit_verify }, { "backup.snapshot", "Create a consistent database snapshot", PERM_ADMIN, 0, 1, 0, h_backup_snapshot }, { "account.list", "List the chart of accounts", PERM_READ, 1, 0, 0, -- cgit v1.3