Skip to content

[MEDIUM] fix(backup): pass config path to verify_password on restore - #23

Open
elhoim wants to merge 2 commits into
AppliedIR:mainfrom
elhoim:fix/restore-verify-password-config-path
Open

elhoim wants to merge 2 commits into
AppliedIR:mainfrom
elhoim:fix/restore-verify-password-config-path

Conversation

@elhoim

@elhoim elhoim commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

BLUF

  • Impact: the post-restore examiner password check silently never ran — the operator is prompted, types a password, and gets no result at all, then believes HMAC verification was confirmed.
  • Root cause: verify_password(examiner, pw) is a 2-argument call to a 3-argument function (verify_password(config_path, analyst, password)); the resulting TypeError was eaten by except (ImportError, Exception): pass.
  • Blast radius: every TTY vhir restore that restored a password hash for the current examiner. The real HMAC verification in vhir review --findings --verify is unaffected, so a wrong password is still caught — just much later.
  • Fix size: 5 insertions / 2 deletions inside one function, plus 2 tests. No signature, output-format or file-format change.
  • Evidence: both new tests fail on unmodified source (the function prints only its header line and returns); 430/430 pass on Python 3.10 and 3.12; ruff check and ruff format --check clean.
  • Re-checked on current main @ b2324ac (2026-10-05): the bug is still present — this PR's new tests, applied to main without the fix, still fail (2 failed). The branch now has main merged in.

Priority: MEDIUM

Defect

src/vhir_cli/commands/backup.py:1145 calls the password verifier with two arguments:

from vhir_cli.approval_auth import verify_password
...
    if verify_password(examiner, pw):
        print("Password correct. HMAC verification will work.")
        return
...
except (ImportError, Exception):
    pass  # approval_auth not available — skip verification

but the function takes three positional parameters (src/vhir_cli/approval_auth.py:249):

def verify_password(
    config_path: Path, analyst: str, password: str, *, passwords_dir: Path | None = None
) -> bool:

So every prompt raises TypeError: verify_password() missing 1 required positional argument: 'password', and except (ImportError, Exception) — which is just except Exception — swallows it.

Why it is wrong

On a TTY restore where the examiner's password hash was restored, the operator sees:

Verification ledger restored for examiner 'steve'.
Enter the examiner password used when findings were approved:

types a password, and the command moves straight on to the OpenSearch restore with no confirmation, no mismatch warning, and no error. The absence of a warning reads as success. The whole point of the prompt — telling the operator now, while the original password is still fresh, that HMAC verification will work — is lost. A wrong or unmatched password only surfaces much later during vhir review --findings --verify, by which time the "run vhir config --setup-password" advice the prompt would have given is far less actionable.

Consistency with sibling code

Every other call site passes the config path first: approval_auth.require_confirmation (approval_auth.py:192) and approval_auth.reset_password (approval_auth.py:336). The command layer resolves it the same way everywhere — config_path = Path.home() / ".vhir" / "config.yaml" in approve.py:48, reject.py:28, review.py:369 and config.py:15. This change simply makes backup.py follow that convention.

Fix

     from vhir_cli.approval_auth import verify_password

+    config_path = Path.home() / ".vhir" / "config.yaml"
     print(f"\nVerification ledger restored for examiner '{examiner}'.")
     while True:
         pw = getpass.getpass(
             "Enter the examiner password used when findings were approved: "
         )
-        if verify_password(examiner, pw):
+        if verify_password(config_path, examiner, pw):
...
-except (ImportError, Exception):
-    pass  # approval_auth not available — skip verification
+except ImportError:
+    pass  # approval_auth not available — skip verification
+except Exception as e:
+    print(f"  Password verification... FAILED: {e}", file=sys.stderr)

The catch-all is split rather than removed. Only ImportError stays silent, which is the case the comment actually describes. Anything else is now reported in the same " <step>... FAILED: {e}" form the surrounding restore steps already use (" Password hash ({examiner_name})... FAILED: {e}", " OpenSearch restore... FAILED: {e}"). Letting the exception propagate instead would abort the remaining OpenSearch restore, ledger restore and the case-activation banner, all of which run after this point — so a visible warning is the right trade, and a future regression of this kind can no longer hide.

Testing

tests/test_backup_restore_password.py drives the real approval_auth.verify_password — not a fake — against a real PBKDF2 password entry written into a temp passwords directory, which is exactly the state a restore leaves behind. Because the real function is the driver, the test checks the arity against the actual signature rather than against an assumption baked into a mock.

  • test_correct_password_is_confirmed — a matching password prints Password correct.
  • test_wrong_password_reports_mismatch — a non-matching password prints the mismatch warning instead of returning silently.

Both fail on the unmodified source, with the function producing nothing but its header line:

E   assert 'Password correct.' in "\nVerification ledger restored for examiner 'steve'.\n"
E   assert 'Password does not match.' in "\nVerification ledger restored for examiner 'steve'.\n"
2 failed in 0.87s

Full suite: 430 passed on Python 3.10 and 3.12 (428 baseline + 2 new). ruff check . and ruff format --check . clean.


CI status

The checks on this PR will show action_required: GitHub gates workflow runs on pull requests from
first-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 on main), using the same steps as .github/workflows/ci.yml:

Step Result
pip install -e ".[dev]" && pytest tests/ — Python 3.10 763 passed
pip install -e ".[dev]" && pytest tests/ — Python 3.11 763 passed
pip install -e ".[dev]" && pytest tests/ — Python 3.12 763 passed
ruff check . + ruff format --check . (latest ruff, unpinned as in CI) check + format clean

🤖 Generated with Claude Code

elhoim and others added 2 commits September 16, 2026 14:03
The post-restore examiner password check never ran. `vhir restore` called
verify_password(examiner, pw) while the function signature is
verify_password(config_path, analyst, password), so every prompt raised
TypeError: verify_password() missing 1 required positional argument:
'password'. The catch-all `except (ImportError, Exception): pass` swallowed
it, so the operator typed a password, saw nothing, and believed HMAC
verification had been confirmed. A wrong or unmatched password only
surfaced much later during `vhir review --findings --verify`.

Pass the config path as the first argument, matching every other call site
(approval_auth.require_confirmation, reset_password) and the sibling
convention `Path.home() / ".vhir" / "config.yaml"` used in approve.py,
reject.py, review.py and config.py. The except clause is split so only
ImportError is silent (approval_auth genuinely absent); any other failure
is reported in the same "  <step>... FAILED: {e}" form the surrounding
restore steps already use, so a regression here can no longer hide, while
the restore still completes.

tests/test_backup_restore_password.py drives the real
approval_auth.verify_password against a real password entry in a temp
passwords dir: one test asserts the matching password prints "Password
correct.", the other asserts a wrong password prints the mismatch warning.
Both fail on the unmodified source, which prints only the header line and
returns silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant