WARDEN-WP-0026 T03: masking display filter (defense-in-depth)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

- warden/mask.py: fingerprint()/mask_value() — presence, length, 8-char sha256
  prefix; never the value.
- proxy.proxy_fetch_fingerprint + `warden access --fingerprint`: masked status view
  (presence/length/hash) that emits no value, so it bypasses the T02 stdout guard.
  Lets two parties compare sha256 prefixes to confirm a shared value without seeing
  it (e.g. rotation landed).
- documented as defense-in-depth (raw bao bypasses it) in OperatorAccessAssist.md
  and the module docstring.
- tests: tests/test_mask.py + CLI fingerprint test. 299 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-16 14:54:55 +02:00
parent 04c8b2ab1d
commit fc0f18aa5c
7 changed files with 150 additions and 3 deletions

46
src/warden/mask.py Normal file
View file

@ -0,0 +1,46 @@
"""Masking display filter for KV values (WARDEN-WP-0026 T03).
Defense-in-depth, **not a boundary**: any place warden would otherwise render a
secret value for a human (a status/listing view) shows a *fingerprint* instead
presence, length, and a short non-reversible hash. Two operators can compare
fingerprints to confirm they hold the same value (e.g. that a rotation landed the
expected token) without either seeing it, and a fingerprint in a transcript
discloses nothing.
Limitation (documented, by design): raw `bao kv get <path>` bypasses this entirely
warden only masks *warden-mediated* output. The real boundary is OpenBao policy
plus the T01 capabilities-safe verify and T02 no-stdout transports.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
# Short, non-reversible hash: first 8 hex chars of SHA-256. Not a value, and a
# collision is irrelevant for the "same/different?" comparison this supports.
_HASH_PREFIX_LEN = 8
@dataclass(frozen=True)
class Fingerprint:
present: bool
length: int
sha256_prefix: str # "" when the value is empty/absent
def render(self) -> str:
if not self.present:
return "absent"
return f"hidden len={self.length} sha256:{self.sha256_prefix}"
def fingerprint(value: str | None) -> Fingerprint:
"""Compute a non-reversible fingerprint of a value. Never returns the value."""
if not value:
return Fingerprint(present=False, length=0, sha256_prefix="")
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:_HASH_PREFIX_LEN]
return Fingerprint(present=True, length=len(value), sha256_prefix=digest)
def mask_value(value: str | None) -> str:
"""Render a value as its masked fingerprint string. Never emits the value."""
return fingerprint(value).render()