Skip to content

The signed trail names which account asked for a change, never which one approved it #249

Description

@vladimirrott

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 <txid>, 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 has the build and test commands, docs/architecture.md covers the trust boundary, and 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

claimedSomeone has said in the thread that they are working on thisenhancementNew feature or requesthardDifficulty: crosses a trust boundary or needs hardwarehelp wantedExtra attention is neededtwir-listedListed in a This Week in Rust CfP. Do not offer to anyone until that issue publishes.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions