Skip to content

fix: artifact verify mints a fresh audit key on hosts without one, turning clean receipts into false TAMPERED verdicts - #4581

Merged
chernistry merged 4 commits into
mainfrom
run-20260825T233445p360736Z
Aug 26, 2026
Merged

fix: artifact verify mints a fresh audit key on hosts without one, turning clean receipts into false TAMPERED verdicts#4581
chernistry merged 4 commits into
mainfrom
run-20260825T233445p360736Z

Conversation

@bernstein-the-conductor

@bernstein-the-conductor bernstein-the-conductor Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes #4532

Problem

bernstein artifact verify gives a third-party verifier a false TAMPERED verdict instead of the designed "HMAC leg skipped" degradation.

Change

Housekeeping, not what this pull request is about:

  • style: apply ruff safe fixes and formatting (53b89b4)
  • [WIP] manager-8b1c0dab partial work (754d956)
  • [WIP] backend-77c7e820 partial work (55dcccf)
bernstein.yaml                                     | 28 ++++++++++++----------
 .../unit/cli/test_artifact_verify_key_handling.py  |  2 +-
 2 files changed, 17 insertions(+), 13 deletions(-)

Verification

  • Host gate before publish: ruff check + pytest tests/unit/cli/test_artifact_verify_key_handling.py - passed.

Provenance

  • Diff: sha256:6b63c12686b42be868f6a581a9077b30458d148867d5d2e2542bde248d3cfdab
  • Journal head: b68cc703ed23c3ad9b6c61dd0a3babfb4d010b115cec9eb983bbadcfadec72bc
  • Verify: bernstein review-receipt verify --pr <this PR> --issue <issue.md> --diff <pr.diff>

Generated from Bernstein session 1787702276.

bernstein-session-id: 1787702276


Made by bernstein v3.18.0 - unattended run run-20260825T233445p360736Z, no operator in the loop.

@bernstein-the-conductor

Copy link
Copy Markdown
Contributor Author

VERDICT: request-changes

Blocking findings

1. src/bernstein/cli/commands/artifact_cmd.py:147_spine_hmac_key() returns bytes | None but callers expect bytes

The PR changes _spine_hmac_key() to use load_audit_key() and return None when the audit key is missing. However, this function is used by artifact log (line 206) and artifact health (line 275) commands, which pass the result to artifact_log(), artifact_attempts(), and artifact_health_json() — all of which have hmac_key: bytes in their signatures (not bytes | None).

This introduces three mypy type errors:

src/bernstein/cli/commands/artifact_cmd.py:206: error: Argument "hmac_key" to "artifact_log" has incompatible type "bytes | None"; expected "bytes"
src/bernstein/cli/commands/artifact_cmd.py:207: error: Argument "hmac_key" to "artifact_attempts" has incompatible type "bytes | None"; expected "bytes"
src/bernstein/cli/commands/artifact_cmd.py:275: error: Argument "hmac_key" to "artifact_health_json" has incompatible type "bytes | None"; expected "bytes"

Root cause: The PR description explicitly notes this as an open decision: "Both call sites in artifact_cmd.py (:132-134 and :348-350) resolve the secret the same wrong way — check whether both should switch or only the verify path (create-side minting may be intended)." The author changed both, but only the verify path (_resolve_operator_secret, used by artifact verify) should switch to load_audit_key(). The _spine_hmac_key() function (used by artifact log and artifact health) should continue using load_or_create_audit_key() because those are operator-facing inspection commands that require a valid HMAC key to verify the chain; they are not third-party verification paths.

Smallest fix: Revert _spine_hmac_key() to use load_or_create_audit_key() and return bytes (not bytes | None):

def _spine_hmac_key() -> bytes:
    """Return the audit-chain key the lineage spine tags entries with."""
    from bernstein.core.security.audit import load_or_create_audit_key

    return load_or_create_audit_key()

Keep the _resolve_operator_secret() change (lines 364-376) as-is — it correctly uses load_audit_key() and returns None for the artifact verify path.

2. Missing release-notes fragment

The PR changes user-visible CLI behavior for artifact verify (it now reports "HMAC leg skipped" instead of false TAMPERED on hosts without an audit key). Per the repo's documentation duty: "User-visible behaviour: update the relevant README.md section" and "A user-visible change -- new CLI surface, changed output, a security property, a removal -- without a fragment IS a blocking finding."

No fragment exists under docs/release-notes/fragments/ for this change.

Smallest fix: Add a fragment file, e.g. docs/release-notes/fragments/4581-artifact-verify-hmac-degradation.md:

## artifact verify degrades gracefully when audit key is missing

`bernstein artifact verify` no longer mints a fresh audit key on hosts that lack one. When the operator HMAC key is absent, the command verifies the Ed25519 signature and chain integrity, skips the HMAC leg, and reports it as skipped-for-missing-key — clearly distinguished from a TAMPERED verdict. This restores the designed offline-verification guarantee for third parties who receive a receipt but do not hold operator secrets. Closes #4532.

Non-blocking observations (Nits)

  • The new test file tests/unit/cli/test_artifact_verify_key_handling.py is well-structured and correctly exercises the acceptance criteria (clean HOME/temp key location, no mocks). Both tests pass.
  • The _resolve_operator_secret() change correctly handles AuditKeyMissingError and falls back to None, matching the library layer's gate.py:201-213 behavior of skipping the HMAC leg when operator_secret=None.
  • All existing artifact verify tests pass (tests/unit/cli/test_artifact_verify_cmd.py — 7 tests).
  • Ruff and pytest pass on the changed files.

Evidence verified

  • Ran uv run pytest tests/unit/cli/test_artifact_verify_cmd.py tests/unit/cli/test_artifact_verify_key_handling.py -x -v — 9 passed
  • Ran uv run ruff check src/bernstein/cli/commands/artifact_cmd.py tests/unit/cli/test_artifact_verify_key_handling.py — clean
  • Ran uv run mypy src/bernstein/cli/commands/artifact_cmd.py tests/unit/cli/test_artifact_verify_key_handling.py — 3 errors (the blocking finding above)
  • Confirmed base behavior via git show origin/main:src/bernstein/cli/commands/artifact_cmd.py — both _spine_hmac_key() and _resolve_operator_secret() used load_or_create_audit_key()
  • Confirmed library layer handles operator_secret=None correctly in src/bernstein/core/lineage/gate.py:201-213

Made by bernstein v3.18.0 - unattended review run run-20260826T025833p750469Z, no operator in the loop.
Signed review receipt: diff_hash sha256:6b63c12686b42be868f6a581a9077b30458d148867d5d2e2542bde248d3cfdab journal_entry_hash sha256:bc5c8b5db9fcf725ace1d52b87014b854dfeaaa8818b5255d4b133b72399b699
(verify with bernstein review-receipt verify)

@bernstein-the-conductor

Copy link
Copy Markdown
Contributor Author

VERDICT: request-changes

Unresolved findings

  1. docs/release-notes/fragments/ — Missing release-notes fragment for the user-visible artifact verify behavior change (skips HMAC leg when audit key missing, reports skipped-for-missing-key instead of false TAMPERED). Required per docs duty: "User-visible behaviour: update the relevant README.md section" and "A user-visible change -- new CLI surface, changed output, a security property, a removal -- without a fragment IS a blocking finding."

Made by bernstein v3.18.0 - unattended review run run-20260826T043440p907854Z, no operator in the loop.
Signed review receipt: diff_hash sha256:7321da647730d1fb3b5f32d7c80a97cdd391ce163eba43b2178c47e5e0051360 journal_entry_hash sha256:f808a6d61a970105783844f00a3c99fdf776dcb59d1af399655b5a0dcd74235d
(verify with bernstein review-receipt verify)

@chernistry
chernistry added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit 436e0a9 Aug 26, 2026
54 checks passed
@chernistry
chernistry deleted the run-20260825T233445p360736Z branch August 26, 2026 11:33
jm27 pushed a commit to jm27/bernstein that referenced this pull request Aug 26, 2026
…ipyourdrink-ltd#4595)

Two changes merged without a release-notes fragment, so neither is
discoverable from the release notes:

- **sipyourdrink-ltd#4473** — plan rendering is deterministic and carries a SHA-256 of
the rendered form.
- **sipyourdrink-ltd#4581** — `bernstein artifact verify` no longer mints a fresh audit
key on a host without one; it previously recomputed every HMAC against
the wrong key and reported clean receipts as `TAMPERED`.

The review flagged the missing fragment on both at the time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

artifact verify mints a fresh audit key on hosts without one, turning clean receipts into false TAMPERED verdicts

1 participant