Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions config/doctor-remediation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,26 @@ layer2_probe:
injector_smoke:
remediation: "Ensure Chromium can launch headless — see docs/DOCTOR.md troubleshooting."
blocker: false

# --- F9 — Chrome binary integrity (4-layer) -------------------------------
# See docs/DOCTOR.md §F9 and docs/VAULT_THREAT_MODEL.md §2.8 for rationale.

f9_l1_codesign:
remediation: "OS-level signature did not verify. Re-install Chrome from its official vendor (Google / Brave / Microsoft / Mozilla) — do not use a binary from a third-party mirror."
blocker: true

f9_l2_sha_pin:
remediation: "SHA-256 not in known-good list. Expected between Chrome releases — no action unless running --strict. To bump the list, open a PR adding the new SHA to pop_pay/data/chrome_known_good_sha256.json."
blocker: false

f9_l3_fork:
remediation: "Vendor not on whitelist. Options: (1) switch to Google/Brave/Edge/Firefox, (2) add --permissive if you trust the detected vendor, (3) confirm the codesign chain is intact with 'codesign -dv --verbose=4 <path>'."
blocker: true

f9_l4_extensions:
remediation: "Audit installed extensions; uninstall any you did not install yourself. pop-pay does not block extensions — this row is informational."
blocker: false

f9_l4_cdp_port:
remediation: "Port conflict — another process is on the CDP port. Stop it, or set POP_CDP_URL=http://localhost:<other-port>. A persistent listener may indicate a hijack attempt; see docs/VAULT_THREAT_MODEL.md §2.9."
blocker: false
31 changes: 28 additions & 3 deletions docs/DOCTOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ documentation; this file notes only the Python differences.

```
$ pop-pay doctor
$ pop-pay doctor --json
$ pop-pay-doctor # equivalent direct entry point
$ pop-pay doctor --json # machine-readable
$ pop-pay doctor --strict # F9: only Google Chrome, SHA must match known-good list
$ pop-pay doctor --permissive # F9: accept any Chrome-family binary with a valid code signature
$ pop-pay-doctor # equivalent direct entry point
$ python -m pop_pay.cli_doctor
```

Exit codes match the TS version: `0` ok / `1` blocker failed / `2` doctor crashed.

## Checks (10 total)
## Checks (15 total)

Same check set as TS, with `python_version` (≥3.10) replacing `node_version`:

Expand All @@ -31,6 +33,28 @@ Same check set as TS, with `python_version` (≥3.10) replacing `node_version`:
| `layer1_probe` | `pop_pay.engine` loads | yes |
| `layer2_probe` | TCP reachability; no API request sent | no |
| `injector_smoke` | `chrome --version` | no |
| `f9_l1_codesign` | Chrome binary has a valid OS code signature | **yes** |
| `f9_l2_sha_pin` | Chrome SHA-256 matches the in-repo known-good list | no (yes under `--strict`) |
| `f9_l3_fork` | Chrome vendor on fork whitelist | **yes** |
| `f9_l4_extensions` | Enumerate installed Chrome-family extensions (informational) | no |
| `f9_l4_cdp_port` | Warn if CDP port is already listening — possible hijack (see VAULT_THREAT_MODEL §2.9) | no |

## F9 — Chrome binary integrity

F9 closes the trust boundary between pop-pay and the Chrome binary it drives over CDP. See `docs/VAULT_THREAT_MODEL.md` §2.8 for the threat model.

**Four layers** (identical semantics to the TS doctor):

1. **OS codesign (primary, load-bearing).** macOS `codesign --verify` + parse of the `Authority=Developer ID Application: <vendor> (<team-id>)` line; Linux `dpkg -V` / `rpm -V`; Windows `Get-AuthenticodeSignature`. Failure blocks startup.
2. **Static SHA-256 pin (secondary).** Chrome executable hashed and compared against `pop_pay/data/chrome_known_good_sha256.json`. Manual PR bump per Chrome major release — the PR review step IS the supply-chain defense. Warn under default; **fail** under `--strict`.
3. **Fork whitelist (tertiary).** Default accepts Google / Brave / Microsoft / Mozilla. `--strict` accepts only Google LLC. `--permissive` accepts any binary with a valid code signature.
4. **Defense-in-depth (runtime).** Extension enumeration across Chrome/Brave/Edge profile dirs + CDP port hijack sniff.

**Never live-fetches `dl.google.com`.** Six-reason rationale is in `VAULT_THREAT_MODEL.md` §2.8.

**Deviation from spec:** macOS L1 uses `codesign --verify` (not `--strict`) because Chrome's `.app` bundle contains resource-fork metadata which `--strict` rejects despite a cryptographically valid signature. Documented in `pop_pay/doctor_f9.py` `_layer1_macos` and in VAULT_THREAT_MODEL §2.8.

**Bumping the known-good list:** open a PR adding an entry to `pop_pay/data/chrome_known_good_sha256.json`. Required fields: `vendor`, `channel`, `version`, `platform`, `arch`, `sha256`. On macOS discover Team ID via `codesign -dv --verbose=4 /Applications/Google\ Chrome.app`.

## Privacy & safety

Expand All @@ -50,6 +74,7 @@ Parsed by an inline minimal YAML-lite parser in `pop_pay/cli_doctor.py`
- **`cdp_port`**: TCP probe only; cannot identify the owning process.
- **`injector_smoke`**: `--version` only, does not boot a headless page.
- **No CATEGORIES policy checks yet.** Gated on S0.2 B-class decision, arriving in S1.1.
- **F9 `f9_l2_sha_pin` lag.** The SHA list is bumped by PR and intentionally trails Chrome's auto-update cadence; a miss under default mode is a *warn* by design. Run `--strict` only in environments where the list can be kept fresh.

## Entry points

Expand Down
46 changes: 43 additions & 3 deletions pop_pay/cli_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
from typing import Literal
from urllib.parse import urlparse

from pop_pay.doctor_f9 import (
F9CheckResult,
F9Options,
ForkMode,
run_f9_checks,
)

CheckStatus = Literal["pass", "warn", "fail"]


Expand Down Expand Up @@ -385,7 +392,25 @@ def _render(checks: list[DoctorCheck]) -> None:
print()


def run_doctor(as_json: bool = False) -> list[DoctorCheck]:
def _f9_to_doctor(r: F9CheckResult, cat: dict[str, dict]) -> DoctorCheck:
"""Adapt an F9CheckResult onto the DoctorCheck surface for rendering.

Any F9 failure is treated as a blocker at the doctor level (mirrors TS).
Under default mode, f9_l2_sha_pin returns warn (not fail), so only the
load-bearing layers (L1 codesign, L3 fork whitelist, and L2 under --strict)
actually surface as blocking fails.
"""
return _mk(
r.id,
r.name,
r.status,
r.detail,
cat,
blocker_override=(r.status == "fail"),
)


def run_doctor(as_json: bool = False, fork_mode: ForkMode = "default") -> list[DoctorCheck]:
cat = _load_remediation_catalog()
checks = [
_check_python_version(cat),
Expand All @@ -399,16 +424,31 @@ def run_doctor(as_json: bool = False) -> list[DoctorCheck]:
_check_layer2_probe(cat),
_check_injector_smoke(cat),
]
# F9 — Chrome binary integrity (4 layers; L4 emits two rows). Never
# live-fetches; see docs/VAULT_THREAT_MODEL.md §2.8.
f9 = run_f9_checks(F9Options(fork_mode=fork_mode, cdp_port=_cdp_port()))
for r in f9.checks:
checks.append(_f9_to_doctor(r, cat))
if as_json:
print(json.dumps([asdict(c) for c in checks], indent=2))
else:
_render(checks)
return checks


def _parse_fork_mode(argv: list[str]) -> ForkMode:
if "--strict" in argv:
return "strict"
if "--permissive" in argv:
return "permissive"
return "default"


def main() -> int:
as_json = "--json" in sys.argv[1:]
checks = run_doctor(as_json=as_json)
argv = sys.argv[1:]
as_json = "--json" in argv
fork_mode = _parse_fork_mode(argv)
checks = run_doctor(as_json=as_json, fork_mode=fork_mode)
return 1 if any(c.status == "fail" and c.blocker for c in checks) else 0


Expand Down
31 changes: 31 additions & 0 deletions pop_pay/data/chrome_known_good_sha256.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$comment": "F9 Layer 2 — static SHA-256 pin list for Chrome-family binaries. Manual PR bump per Chrome major release; the PR review step IS the supply-chain defense (see VAULT_THREAT_MODEL.md §2.8). NOT auto-fetched. Keys are vendor/channel/version/platform/arch; values are hex SHA-256 of the Chrome executable (macOS: Contents/MacOS/<Binary>; Linux: the google-chrome/chromium binary; Windows: chrome.exe). Layer 3 vendor whitelist matches the 'Authority=Developer ID Application: <vendor> (<team-id>)' string from codesign on macOS and the publisher CN on Windows; team-id map below is a nice-to-have confirmation seed, not a hard requirement — entries added here must be self-verified by PR reviewer.",
"schema_version": 1,
"updated": "2026-04-21",
"entries": [
{
"vendor": "Google LLC",
"channel": "stable",
"version": "147.0.7727.56",
"platform": "darwin",
"arch": "universal",
"sha256": "0acf99fc08a6ea8b67505c507813c0e9bdc983b615fa4301a878f17aec5fb889",
"note": "Seed entry — discovered from operator install on 2026-04-21 (macOS universal binary, x86_64+arm64)."
}
],
"platforms_pending_seed": [
"linux/x64 (google-chrome-stable / chromium)",
"linux/arm64",
"win32/x64",
"win32/arm64"
],
"vendors_accepted_default": [
"Google LLC",
"Brave Software Inc.",
"Microsoft Corporation",
"Mozilla Foundation"
],
"vendor_id_macos_known": {
"Google LLC": "EQHXZ8M8AV"
}
}
Loading
Loading