diff --git a/docs/PHI.md b/docs/PHI.md index 27c8fbb..d7a7386 100644 --- a/docs/PHI.md +++ b/docs/PHI.md @@ -598,12 +598,22 @@ start**, and refuses only if the estate opts in via `[security].require_memory_e That turns the deployment requirement from prose into a declaration of record with a standing warning where it is absent. -**The scorecard is unchanged, and this document does not pre-empt it.** ADR 0152 holds that rung 2 is -what would justify `Fail → Partial`, and the evidence for that now exists — but 11.7.1 is scored on the -ASVS L3 assessment of record (`ASVS-L3-ASSESSMENT-2026-07-22.md`), which still reads **Fail**, -and re-scoring it is an owner decision rather than a side effect of shipping a build. A **Pass** would -in any case require SEV-SNP/TDX hardware plus a verified CPU-signed quote (ADR 0152 rung 3, not built), -and even then the CPython-heap residual above is unchanged. +**This document does not pre-empt the scorecard.** 11.7.1 is scored **`na`**: the requirement's verb is +*"full memory encryption is in use"* — a property of the hosting substrate, which +[`ASVS-ASSESSMENT-METHOD.md`](ASVS-ASSESSMENT-METHOD.md) §2 places outside the assessed software, so +rule 1 takes it out of scope. Re-scoring remains an owner decision rather than a side effect of +shipping a build. + +⚠️ **The record is the scorecard itself** — `docs/security/asvs-scorecard.toml`, rendered and CI-gated +([ADR 0156](adr/0156-asvs-scorecard-as-data-a-derived-count-verified-evidence-anchors-and-a-fail-closed-drift-gate.md)) +— **never a prose assessment.** ADR 0156 replaced the dated-document lineage precisely because prose +asserts facts about code and the code moves; do not cite a dated assessment file as the verdict of +record. *(This paragraph previously did exactly that, naming a dated assessment and reporting a `Fail` +that the record no longer carried.)* + +**An out-of-scope verdict buys nothing operationally**, which is the point of §2.1: the CPython-heap +residual above is unchanged either way, and a deployment still needs the host-side control. Rung 3 +(SEV-SNP/TDX plus a verified CPU-signed quote) remains unbuilt. --- diff --git a/docs/adr/0152-in-use-data-protection-for-phi-platform-memory-encryption-attestation-asvs-11-7-1.md b/docs/adr/0152-in-use-data-protection-for-phi-platform-memory-encryption-attestation-asvs-11-7-1.md index 09f6963..0c2dd95 100644 --- a/docs/adr/0152-in-use-data-protection-for-phi-platform-memory-encryption-attestation-asvs-11-7-1.md +++ b/docs/adr/0152-in-use-data-protection-for-phi-platform-memory-encryption-attestation-asvs-11-7-1.md @@ -10,7 +10,7 @@ Build state at acceptance: | | | |---|---| -| **Rung 1** — platform read-out, report-only | **Built.** `config/memory_encryption.py`; four `memory_encryption_self_reported_*` / `..._readout_source` fields on `GET /security/posture`, plus the in-body disclaimer `memory_encryption_note` (`ENGINE_UI_SEAM` 12 → 13). | +| **Rung 1** — platform read-out, report-only | **Built.** `config/memory_encryption.py`; **seven** `memory_encryption*` fields on `GET /security/posture` — three `_self_reported_*`, plus `_readout_source`, `_operator_declared`, `_readout_contradicts_declaration`, and the in-body disclaimer `memory_encryption_note`. *(Corrected 2026-08-02: this row said "four", counting only the read-out fields and omitting the declaration, contradiction and note fields; [`api/models.py`](../../messagefoundry/api/models.py) makes the same 3+1 slip in its own comment. On the seam: `ENGINE_UI_SEAM` moved 12 → 13 for this work — release history, carried correctly in `CHANGELOG.md` — but this row is present-tense "Built.", so read as current state it was wrong; the seam is **16** today.)* | | **Rung 2** — operator declaration | **Built.** `[security].memory_encryption_operator_declared` (default `false`); an **exposed PHI** instance without it **warns at every start**, and refuses only when the estate opts in via `[security].require_memory_encryption_declaration` (default `false`) under `enforcement = enforce`. A contradiction is warned + reported as the tri-state `memory_encryption_readout_contradicts_declaration`, never refused. See the *2026-07-22 amendment* below for why the refusal is opt-in and why the field is not called "attested". | | **Rung 3** — cryptographic attestation | **Not built.** No quote acquisition, no signature verification, no vendor-chain handling. | | **Windows rung 3** | **Recorded infeasible in practice** — see *Windows rung 3 — spike conclusion* below. It is **platform-blocked, not API-blocked**. | @@ -39,11 +39,18 @@ it because L3 is the stated target for a PHI system, not because the conventiona **Memory hygiene is not memory encryption, and OWASP has already ruled on that distinction.** During the 4.x→5.0 cull OWASP **deleted** 4.0.3's V8.3.6 (overwrite sensitive memory when no longer needed) as *"NOT PRACTICAL"* — and **kept** 11.7.1. So the zeroization/locking family is explicitly not what -this requirement asks for. We already ship that family and it does not move this cell: `_secure_zero` -over a `bytearray` via `ctypes.memset` ([`store/crypto.py:163`](../../messagefoundry/store/crypto.py)), -`VirtualLock`/`mlock` residency ([`:185-199`](../../messagefoundry/store/crypto.py)), DEK zeroization -after install ([`:314-325`](../../messagefoundry/store/crypto.py)) and plaintext zeroization -([`:462`](../../messagefoundry/store/crypto.py)) all landed in #198. +this requirement asks for. We already ship that family and it does not move this cell — all of it in +[`store/crypto.py`](../../messagefoundry/store/crypto.py): `_secure_zero()` over a `bytearray` via +`ctypes.memset`, `VirtualLock`/`mlock` residency in `_lock_memory()` / `_unlock_memory()`, DEK +zeroization after install in `_install_key()`, and plaintext zeroization at the `_secure_zero(pt)` +calls on the encrypt and decrypt paths. All landed in #198. + +*(Anchors corrected 2026-08-02, and the claim is unchanged — only the pointers were wrong. These were +line citations: `:163`, `:185-199`, `:314-325`, `:462`. Every one had rotted, and the first two had +come to point at **each other's** code — `:163` at prose inside `cell_aad()`'s docstring, `:185-199` at +the body of `_secure_zero` rather than the residency helpers. Re-anchored by **symbol**: these render as +markdown link text over a target carrying no line fragment, so the link always resolves and no link +checker can catch this class. New line numbers would rot the same way at the next refactor.)* **CPython can protect keys but not message bodies, and that asymmetry is structural.** A DEK is small, short-lived, and we own the buffer — hence the work above. An HL7 message is not: it is `str` end to