ops-warden/src/warden/mask.py
tegwick fc0f18aa5c
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
WARDEN-WP-0026 T03: masking display filter (defense-in-depth)
- 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>
2026-07-16 14:54:55 +02:00

46 lines
1.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()