Repository navigation
Conversation
A case backup copies the HMAC verification ledger but selected which examiner password-hash files to include from each finding's created_by field. Ledger entries are keyed by approved_by, so whenever the examiner who approved an item is not the one who staged it, the approver's salt is never backed up. After restoring on another machine, `vhir review --findings --verify` prompts for the approver's password, get_analyst_salt raises ValueError and review prints "Skipped: No salt found for analyst <name>" — the entire chain of custody for the case is unverifiable. Findings carrying no created_by at all backed up zero hashes alongside a full ledger. The examiner set is now derived from the distinct approved_by values in the ledger file actually being copied, unioned with created_by so the previous behaviour is preserved and staging examiners keep their salts. approved_by is what verify_items() and rehmac_entries() filter on, so this is the only set that makes the restored ledger verifiable. It is also the only complete one: the ledger holds timeline and IOC entries too (approve.py sets item_type), which findings.json can never account for. The restore path already installs every passwords/*.json it finds without cross-checking names, so no change is needed there. tests/test_backup.py covers both halves: a case staged by 'dana' and approved by 'ravi' must back up both salts, and a case whose findings have no created_by must still back up the approver's. Against the unmodified source these fail with ['dana'] != ['dana', 'ravi'] and [] != ['ravi']. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolve the conflict in commands/backup.py by keeping this branch's structure (collect ledger approvers and finding creators, then copy their password hashes in one loop) and applying upstream's 87cf91a hardening to both sources: only names that are strings matching _EXAMINER_RE are backed up, for created_by as upstream does and now also for the ledger's approved_by. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
[pr-sweep] withdrawn: upstream b100718 (backups hold no credential, restores add keys as retired) made a case backup carry no password file at all, by design: the signed history carries the public keys that verify it. There is no salt selection left for this PR to fix, and adding approver salts back would go against that decision. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BLUF
vhir review --findings --verifyprintsSkipped: No salt found for analyst <name>and the entire HMAC chain of custody for the case is silently unavailable.created_by, but ledger entries are keyed byapproved_by. Whenever the approver is not the stager, the approver's salt is never backed up.created_byat all, which backs up zero hashes beside a full ledger. Timeline and IOC ledger entries were never covered byfindings.jsonunder any circumstance.approved_byvalues in the ledger file already being copied, unioned withcreated_byso nothing that used to be backed up stops being backed up.passwords/*.jsonit finds, so no restore-side change is needed.tests/test_backup.py(2 tests) fails on unmodified source with['dana'] != ['dana', 'ravi']and[] != ['ravi']; 430/430 green on Python 3.10 and 3.12, ruff clean.main@b2324ac(2026-10-05): the bug is still present — this PR's new tests, applied tomainwithout the fix, still fail (2 failed). The branch now hasmainmerged in.Priority: HIGH
The defect
create_backup_datacopies the HMAC verification ledger into the backup, then copies the examiner password-hash files needed to verify it. The two are selected from different fields:Ledger entries are written with
approved_by, notcreated_by:and that is the only field verification ever looks at:
So the backup copies a ledger signed by the approvers and the salts of the stagers.
Why it is wrong
Junior examiner
danastages findings (created_by = dana); senior examinerraviapproves them, so every entry inverification.jsonlhasapproved_by = ravi. The backup copiesdana.jsonand the manifest advertisespassword_examiners: ["dana"]. On another machine, restore installs dana's hash and the ledger. Then:get_analyst_saltraisesValueError,review.py:407-408swallows it, and the chain of custody for the whole case is unverifiable — from a backup that reported success and listed apassword_examinersset. The same happens, more completely, whenever findings carry nocreated_byat all: zero hashes are backed up alongside a full ledger.Consistency argument
approved_byis the field both consumers of the ledger filter on —verify_items(verification.py:87) andrehmac_entries(verification.py:127). It is also the only complete source: the ledger holdstimelineandiocentries as well as findings (approve.pyderivesitem_typefrom the item id prefix), sofindings.jsoncould never account for every signer even in the single-examiner case. Andmain.py:108documents--byas "Filter items by creator examiner", confirmingcreated_byis the stager, not the approver.The restore path needs no change: it globs
backup_dir/passwords/*.jsonand installs each one without cross-checking names against findings, so once the salt is in the backup it is restored.The fix
Derive the set from the ledger file that is actually being copied, and union it with
created_byso the previous behavior is strictly preserved:followed by the unchanged
created_byblock, now calling.update()on the same set, and the unchanged copy loop. Readingledger_pathrather thanverification.read_ledger()is deliberate:ledger_pathis the exact file being copied, so the derived set is by construction the set of signers present in the backup. Each of the three stages keeps its own best-efforttry/except, so a ledger read failure cannot abort the pre-existingcreated_bycopies.Testing
New
tests/test_backup.py, two tests, monkeypatchingbackup._PASSWORDS_DIRandbackup.VERIFICATION_DIRat temp paths so nothing touches/var/lib/vhir:test_backup_includes_approver_password_hash— findings staged bydana, ledger approved byravi; asserts both salts land in the backup and in the manifest.test_backup_includes_approver_without_created_by— findings with nocreated_by; asserts the approver's salt is still backed up.Against unmodified source (verified by reverting the source file and re-running only these tests):
With the fix: 430 passed on Python 3.10 and Python 3.12 (428 baseline + 2),
ruff checkandruff format --checkclean.CI status
The checks on this PR will show
action_required: GitHub gates workflow runs on pull requests fromfirst-time fork contributors until a maintainer approves them. They need an Approve and run click.
The CI job was reproduced locally after merging
main@b2324ac(761 tests onmain), using the same steps as.github/workflows/ci.yml:pip install -e ".[dev]" && pytest tests/— Python 3.10pip install -e ".[dev]" && pytest tests/— Python 3.11pip install -e ".[dev]" && pytest tests/— Python 3.12ruff check .+ruff format --check .(latest ruff, unpinned as in CI)🤖 Generated with Claude Code