Skip to main content

Module audit_chain

Module audit_chain 

Source
Expand description

Forward Ed25519-signed hash chain for the audit log.

Each transactions row stores chain_hash and prev_chain_hash columns. On insert, the daemon computes

chain_hash = ed25519_sign(ROW_DOMAIN || canonical(immutable_fields) || prev_chain_hash, signing_key)

The ROW_DOMAIN prefix is part of the signed message, not decoration: it is what stops a row signature from ever verifying as a checkpoint or approval-receipt signature. An independent verifier that omits it will never reproduce a valid chain_hash.

and stores both (the signature is hex-encoded; the first row in a chain has prev_chain_hash = ""). Ed25519 signatures are deterministic (RFC 8032), so the same content always produces the same chain_hash.

Verification (sysknife audit verify) walks rows in seq order and checks each row’s signature with the public key, reporting the first broken link. Because verification needs only the public key, an auditor or a central log aggregator can verify the chain without holding the private key — they cannot forge entries. This is the property a symmetric MAC (the previous HMAC-SHA256 design) could not provide: with an HMAC, the verifier and the forger are the same principal.

§Threat model

  • Non-repudiation (asymmetric). The daemon signs with the private key; anyone with the exported public key (<key>.pub) can verify but not forge. A compromise of the verifier does not enable forgery.
  • Compromised host / root. An attacker who reads the private key file can forge future entries. Mitigation is to anchor signed checkpoints to an append-only external sink (see checkpoint_sink) so that later tampering with past entries becomes detectable and the tamper window is bounded to “after compromise”.
  • Tail truncation is undetectable by the chain walk alone: an attacker who deletes the last K rows leaves a still-consistent chain. It is caught by anchoring signed checkpoints to an independent append-only sink, since a truncated chain can no longer reproduce a previously anchored (seq, chain_tip) (see verify_checkpoints and checkpoint_sink). The best-effort audit_watermark journald forward is a lighter complement.
  • In-flight modification between insert and read is mitigated by computing the signature before INSERT and writing it in the same SQL statement.
  • Status mutations are not in the chain. The mutable status field is intentionally excluded — the chain protects the authorisation decision (immutable fields captured at insert time), not the live execution state. A future append-only audit_events table will chain status transitions if the threat model demands it.

§Key management

The Ed25519 private key (a 32-byte seed) lives in a file. By default the path is <db_dir>/audit-key (sibling of the SQLite database, or of whatever directory sysknife_core::default_database_path resolves to in production), and the env var SYSKNIFE_AUDIT_KEY_PATH overrides it for systemd unit drop-ins (typically /etc/sysknife/audit-key). The file is created with mode 0o600 on first daemon start if it does not exist; subsequent runs refuse to start if it is world-readable. The public key is written alongside as <key>.pub (hex) for auditors and aggregators.

Future epochs (key rotation): each row already carries a key_id column. A planned rotation flow appends a checkpoint row signed with the outgoing key whose payload references both public-key fingerprints; verification walks the chain through epoch boundaries by looking up each row’s key_id in a directory of retired public keys. For now, all rows use key_id = "v1" and rotation is manual (delete the chain, regenerate).

Structs§

AuditKey
Loaded Ed25519 signing key + its identifier. Construct via AuditKey::load_or_generate.
AuditVerification
Everything sysknife audit verify checks, in one value.
ChainContent
ChainRow
One row’s worth of chain data, as fetched from the store.
Checkpoint
A signed commitment to the chain tip at a point in time. Periodically emitted and anchored to an independent, append-only sink (a separate database, a WORM store, or an RFC 3161 timestamp) so that a later attempt to rewrite or truncate the local chain is detectable: the anchored (seq, chain_tip) can no longer be reproduced from the shortened chain.
EventContent
Immutable content of one approval event, signed into the event chain.
EventRow
One approval event as fetched from the store.

Enums§

AuditEventKind
A lifecycle event on an approval receipt.
AuditKeyError
BindingOutcome
Result of checking that the transaction chain’s committed event_tip values are still reproducible from the event chain.
ChainIdentity
Immutable fields hashed into the chain. Status is intentionally absent — see module docs.
CheckpointOutcome
Result of checking anchored checkpoints against the current chain.
VerifyOutcome
Result of verify_chain.

Constants§

CHAIN_VERSION_CURRENT
Row encoding written by this binary: adds caller_role and event_tip.
CHAIN_VERSION_LEGACY
Row encoding written before the caller-identity migration: no caller_role, no event_tip. Still verifiable — see ChainIdentity.
CURRENT_KEY_ID
Stable identifier for the current key generation. Stored in every row. Tied to the schema, not the key bytes — rotation will introduce "v2" etc.
HASH_HEX_LEN
Hex-encoded length of an Ed25519 signature (64 raw bytes → 128 hex chars).

Functions§

approval_receipt_digest
binding_outcome_to_exit_code
Exit code for the binding check, on the same scale as outcome_to_exit_code: a missing event is a detected tamper (1).
checkpoint_outcome_to_exit_code
Process exit code for sysknife audit checkpoint verification, mirroring outcome_to_exit_code: 0 consistent, 1 a detected tamper (bad signature / truncation / rewrite), 2 cannot-verify (e.g. malformed public key). The 1-vs-2 split matters for CI exactly as it does for chain verify.
outcome_to_exit_code
Process exit code for sysknife audit verify.
resolve_audit_key_path
Resolve the audit key file path: $SYSKNIFE_AUDIT_KEY_PATH if set, otherwise <db_dir>/audit-key (sibling of the database). This is the single definition of the key-location precedence documented in the module header; the CLI verify/checkpoint paths resolve through it so they always agree with the daemon on where the key lives.
verify_all
Run all three checks with the daemon’s key.
verify_all_with_pubkey
Run all three checks with only the exported public key (the auditor path).
verify_chain
Verify a chain using the daemon’s key. Verification uses the public key (so it proves, but cannot forge) and also asserts every row was written under this key generation (key_id).
verify_chain_with_pubkey
Verify a chain with only the hex-encoded Ed25519 public key. This is the auditor / aggregator path: it proves the chain without the private key and cannot be used to forge entries. key_id is not checked — the public key itself identifies the signer.
verify_checkpoints
Verify anchored checkpoints against rows (the current chain) with the hex public key. Detects truncation (a checkpoint seq no longer in the chain) and rewrite (tip mismatch at a checkpoint seq). rows must be in seq order. This is the anti-root guarantee: a host attacker who shortens or edits the local chain cannot reproduce a previously anchored signed tip.
verify_event_binding
Check the cross-chain binding: each chain_version = 2 transaction row signs the event-chain tip as of its insert, so a later deletion of trailing approval events leaves a tip that can no longer be found.
verify_event_chain
Verify the approval-event chain with the daemon’s key.
verify_event_chain_with_pubkey
Verify the approval-event chain with only the exported public key.