aboutsummaryrefslogtreecommitdiff
path: root/src/commands.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 09:20:39 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 09:20:39 +0200
commit5b27bb0950fd03d29c223b5960faff89baf37970 (patch)
tree5cc99420a4fc1365fdb8bf4bbec8237fa306fd4b /src/commands.c
parenta61cfeefb0599e74fc9189b189b16dd0b6d36f0f (diff)
downloadbokf-5b27bb0950fd03d29c223b5960faff89baf37970.tar.gz
bokf-5b27bb0950fd03d29c223b5960faff89baf37970.zip
audit: verify voucher chains and attachment hashes
Diffstat (limited to 'src/commands.c')
-rw-r--r--src/commands.c114
1 files changed, 104 insertions, 10 deletions
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,