lacs-project / lacs-project/sysknife
The signed trail names which account asked for a change, never which one approved it
- Dominant language
- Rust
- Stars
- 12
- Forks
- 19
- Avg merge
- 18h 57m
- Merged PRs (30d)
- 116
Description
The transaction chain signs `caller_principal`, captured in `handle_preview` at `crates/sysknife-daemon/src/dispatcher.rs:2106`, so a verified row answers which account asked for an action. `ChainIdentity::V3` states why the field exists (`crates/sysknife-daemon/src/audit_chain.rs:466`):
> v2 answers "an Admin did this". On a host with two members of `sysknife-admin` their signed records were indistinguishable, so the trail could not answer the first question an investigation asks.
The approval chain never got the same treatment. `EventContent::canonical_bytes` signs six fields, and no account appears among them (`crates/sysknife-daemon/src/audit_chain.rs:934`):
```
seq, key_id, kind, transaction_id, receipt_digest, created_at
```
`TransactionStore::approve_transaction` takes a transaction id and nothing else (`crates/sysknife-daemon/src/transactions.rs:548`), while `handle_approve` holds a `CallerAttribution` and passes it to `authorize_for_transaction` a few lines earlier (`crates/sysknife-daemon/src/dispatcher.rs:1361`). `claim_approved_for_execution` is the same (`transactions.rs:652`): the account that spends the receipt is never written down.
## What it costs
Two admins on one host, alice at uid 1000 and bob at uid 1001, both in `sysknife-admin`. Alice's agent previews `AddAuthorizedKey`, bob types `sysknife approve `, alice's agent executes. The verified trail then holds:
| record | what it names |
|---|---|
| transaction row | `caller_principal = uid:1000`, alice, who previewed |
| `approval_granted` | transaction id, receipt digest |
| `approval_consumed` | transaction id, receipt digest |
Nothing names bob. The receipt exists so that a human authorises what an agent proposed, and the chain cannot say which human that was.
`docs/automatic-rollback.md:130` describes the signed fields as "the authorization decision — who approved what, at what risk level". They record who asked.
## Why this is more than an ALTER TABLE
`transactions` versions its signed encoding per row through `chain_version`, added in migration 2 so rows written by an older binary keep verifying (`crates/sysknife-daemon/src/transactions.rs:171`). `audit_events` has no such column in either backend (`transactions.rs:179`, `crates/sysknife-daemon/src/store/postgres.rs:119`). Add a field to `EventContent` without versioning the encoding and every event row already on disk reports `Broken`, because its signature covers the six-field message.
## Suggested shape
1. Migration 4 in both `SQLITE_MIGRATIONS` and the Postgres `MIGRATIONS`: `audit_events.chain_version` defaulting to 1, `audit_events.caller_principal` nullable, for the reason spelled out in the migration-2 comment.
2. An `EventIdentity` enum beside `ChainIdentity`, where `LegacyV1` signs today's six fields and `V2` appends the principal, with `event_message` selecting per row.
3. Thread `CallerPrincipal` into `approve_transaction`, `claim_approved_for_execution` and `revoke_unconsumed_approval`, from the `CallerAttribution` the handlers already hold.
4. Keep `sysknife audit verify` correct over a mixed-version event chain.
## Acceptance
- An event row written before the change still verifies, against a committed fixture rather than one the test regenerates.
- A grant and a consume by different uids are distinguishable in the chain.
- Editing a stored `caller_principal` on an event reports `Broken`.
- Both backends. `crates/sysknife-daemon/tests/postgres_store.rs:298` is the pattern for the Postgres side.
Difficulty: hard. Two storage backends, a signed encoding, and a compatibility fixture.
## Getting started
[CONTRIBUTING.md](https://github.com/lacs-project/sysknife/blob/main/CONTRIBUTING.md) has the build and test commands, [docs/architecture.md](https://github.com/lacs-project/sysknife/blob/main/docs/architecture.md) covers the trust boundary, and [docs/the-audit-chain.md](https://github.com/lacs-project/sysknife/blob/main/docs/the-audit-chain.md) explains what the chain signs today. No CLA and no copyright waiver. The project is MIT.
The Postgres half needs a local database. `podman run -d -e POSTGRES_USER=sysknife -e POSTGRES_PASSWORD=sysknife -e POSTGRES_DB=sysknife_test -p 5432:5432 docker.io/library/postgres:17-alpine`, then run the contract suite with `SYSKNIFE_TEST_POSTGRES_URL` and `SYSKNIFE_REQUIRE_POSTGRES=1` set and `-- --include-ignored`.
Comment here before you start. The migration ordering is worth agreeing on before any code exists.
Contributor guide
Research direction
Start with EventContent::canonical_bytes and ChainIdentity in crates/sysknife-daemon/src/audit_chain.rs, then trace migrations and approval methods in transactions.rs and the handlers in dispatcher.rs. Compare the SQLite and Postgres migrations, and use crates/sysknife-daemon/tests/postgres_store.rs:298 plus the existing audit verification tests. Done means legacy fixtures verify, grant and consume identities are distinct, tampering reports Broken, and both backends pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, rust, sqlite
- Domain
- backend, databases, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100