Reject a decrypted snapshot database that is not the requested one #194

Merged
clawbot merged 1 commits from issue-156-verify-snapshot-db-identity into next 2026-09-22 15:01:02 +02:00
Collaborator

Restore and deep verify downloaded metadata/<key>/db.zst.age, decrypted it, and then trusted it by object name alone. age decryption only proves a database is readable, not that it is the snapshot requested: an attacker could swap in another valid db.zst.age (no key material needed) to redirect restore to a different snapshot's files, and deep verify with a swapped database plus an empty manifest reported success with "Blobs verified: 0".

Both paths now check identity immediately after opening the database, before any row is trusted. An exported per-snapshot database holds exactly one snapshot row, and a snapshot's remote key is derived from that row's ID, so the database is the requested one exactly when its sole snapshot hashes back to the remote key it was fetched under. This is stronger than comparing the requested identifier directly — and necessary, because a recovery host may pass a remote-key prefix in place of the human ID it cannot know (see TestRestoreOnAnotherMachine).

New SnapshotRepository.GetOnlySnapshot returns that sole row (or reports zero / multiple); the shared verifySnapshotDBIdentity does the key comparison, and restore and deep verify both call it. internal/database cannot import internal/snapshot, so the key derivation stays on the vaultik side.

Judgement call: loadVerificationData would exceed the length limit, so its database-open block moved into a new downloadVerifiedSnapshotDB — the only structural change to verify.go, worth care when rebasing over #193.

Model: opus-4-8

Restore and deep verify downloaded `metadata/<key>/db.zst.age`, decrypted it, and then trusted it by object name alone. age decryption only proves a database is readable, not that it is the snapshot requested: an attacker could swap in another valid `db.zst.age` (no key material needed) to redirect restore to a different snapshot's files, and deep verify with a swapped database plus an empty manifest reported success with "Blobs verified: 0". Both paths now check identity immediately after opening the database, before any row is trusted. An exported per-snapshot database holds exactly one snapshot row, and a snapshot's remote key is derived from that row's ID, so the database is the requested one exactly when its sole snapshot hashes back to the remote key it was fetched under. This is stronger than comparing the requested identifier directly — and necessary, because a recovery host may pass a remote-key prefix in place of the human ID it cannot know (see `TestRestoreOnAnotherMachine`). New `SnapshotRepository.GetOnlySnapshot` returns that sole row (or reports zero / multiple); the shared `verifySnapshotDBIdentity` does the key comparison, and restore and deep verify both call it. `internal/database` cannot import `internal/snapshot`, so the key derivation stays on the vaultik side. Judgement call: `loadVerificationData` would exceed the length limit, so its database-open block moved into a new `downloadVerifiedSnapshotDB` — the only structural change to `verify.go`, worth care when rebasing over https://git.eeqj.de/sneak/vaultik/pulls/193. Model: opus-4-8
clawbot added 1 commit 2026-09-22 14:28:30 +02:00
Restore and deep verify downloaded and decrypted metadata/<key>/db.zst.age
by object name alone. age decryption proves the database is readable, not
that it is the snapshot that was asked for: an attacker who swaps in another
valid db.zst.age (no key material needed) could redirect the operation to a
different snapshot, and deep verify with a swapped database plus an empty
manifest reported success with zero blobs verified.

After the database is opened, both paths now confirm its identity: an
exported per-snapshot database holds exactly one snapshot row, and a
snapshot's remote key is derived from that row's ID, so the database is the
requested one exactly when its sole snapshot hashes back to the remote key
fetched. Comparing the requested identifier directly would wrongly reject a
recovery host that supplies a remote-key prefix in place of the human ID it
cannot know.

The shared check lives in verifySnapshotDBIdentity, backed by a new
SnapshotRepository.GetOnlySnapshot; both restore and deep verify call it.

Model: opus-4-8
clawbot added the needs-review label 2026-09-22 14:28:38 +02:00
clawbot self-assigned this 2026-09-22 14:28:38 +02:00
Author
Collaborator

PASS

Model: opus-4-8

PASS Model: opus-4-8
clawbot merged commit bd9656dbd4 into next 2026-09-22 15:01:02 +02:00
clawbot deleted branch issue-156-verify-snapshot-db-identity 2026-09-22 15:01:03 +02:00
Sign in to join this conversation.