diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6c0ec..f7c4c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ The "resource-bound authority" release: delegations can now name *where* a scope - **Input bounds** (SPEC §5.1): `MAX_PROOF_BUNDLE_BYTES` (128 KiB, enforced before parsing), `MAX_JSON_NESTING_DEPTH`, and per-cert scope/constraint count and length limits. Violations route to the existing `invalid` status. - **Conformance suite** grows from 63 to 79 canonical vectors: 16 new verify-kind fixtures (14 `resource_path` accept/deny/unverifiable/narrowing/escape/traversal/percent-literal/root/trailing-slash/whole-resource/unsatisfiable-pair, 1 extension-`params`, 1 depth-8), with `reject_chain_too_deep` regenerated at depth 9. Byte-identical across all five SDKs. - **`VerifierContext` resource fields** (SPEC §5.16): `RequestedResourceID`, `RequestedPath`, `HasResource`, with the standard fail-closed absence semantics. -- **Confinement guidance** (SPEC §15.7): a verified `resource_path` is lexical authorization, not filesystem confinement; receiving systems must enforce containment (root resolution, symlink rejection, containment re-checks, sandboxing) as a separate layer. +- **Confinement guidance** (SPEC §15.7): a verified `resource_path` is lexical authorization, not filesystem confinement. Stated as normative properties rather than a prescribed mechanism: the deployment states its concurrent-mutation attacker model; a confinement or policy refusal leaves no operation-created effect inside or outside the boundary (with identity-safe cleanup that never touches objects a concurrent principal substituted); execution causes no effect outside the boundary; write atomicity is a separate contract; and refusal tests must attribute effects to the operation rather than assert blanket snapshot equality. Descriptor-relative traversal is the illustrative mechanism, not a mandate. ### Changed diff --git a/SPEC.md b/SPEC.md index f3a0d6f..6427f0f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1527,7 +1527,17 @@ Consequences: - Constraints defend the **principal** against agent overreach at an honest verifier. They do not defend anyone against a dishonest or compromised verifier, which can trivially supply satisfying context (see also threat T8 — the malicious-verifier row — and §10: the verifier is the party running the algorithm; it can also simply skip it). - A third party auditing a transaction after the fact cannot independently confirm constraint context from the protocol artifacts alone. Deployments that need auditable constraint claims SHOULD bind the evaluated context into a signed artifact — include it in the `VerificationReceipt` (§17.5) or in `TransactionReceipt.terms` (§5.14) — so the claim is at least signed and attributable to the verifier that made it. - Where context must be *proven* rather than asserted (regulated geofencing, financial limits), the context source itself needs attestation — signed GNSS fixes, platform attestation, or a co-signing oracle — which is outside the protocol. The `ConstraintEvaluator` extension point (§17.7) is where such attested evaluators plug in. -- A verified `resource_path` constraint (§5.7.3) is **lexical** authorization over a logical path, not filesystem confinement. A receiving system that maps logical paths onto a real filesystem MUST enforce confinement itself: apply every decoding and normalization step (URL decoding, platform Unicode normalization, case folding, separator conversion, path cleanup) **before** constructing `RequestedPath` and never transform the verified path afterward (§5.7.3); resolve the confinement root once at startup and operate on the resolved root; resolve each requested path's nearest existing parent and verify containment before any create or write; reject symlinked components between root and target; prefer directory-relative open APIs over path-string concatenation; re-check containment after create and rename operations; and run the executing worker inside a sandbox whose filesystem view is the confinement root. The cryptographic guarantee and the confinement guarantee are separate layers and should be reported separately: Ratify proves authorization for a canonical logical resource and path; the adapter attests context; the execution layer enforces filesystem confinement. +- A verified `resource_path` constraint (§5.7.3) is **lexical** authorization over a logical path, not filesystem confinement. Ratify proves authorization for a canonical logical resource and path; a receiving system that maps those logical paths onto a real filesystem MUST enforce confinement itself. Two properties below are normative for that layer; the mechanisms that achieve them are deployment choices, not protocol requirements. + + **Normative properties.** (1) Under the deployment's **stated attacker model**, a confinement or policy refusal reached before the write commits MUST NOT modify or remove any pre-existing filesystem object, MUST NOT write the attempted payload, and MUST NOT leave behind any filesystem object created by the refused operation, whether inside or outside the confinement boundary. An implementation MAY create temporary or internal artifacts while resolving or staging the write, provided a refusal leaves no externally persistent effect: cleanup MUST act only on artifacts this invocation created whose identities still match what it created and which remain safe to remove, and MUST NOT follow paths through, remove, or modify objects a concurrent principal may have substituted. (2) Execution MUST NOT cause any filesystem effect outside the confinement boundary. The confinement property is undefined without a stated attacker model: the integrator MUST state which principals may rename, link, or replace objects under the root while an operation is in progress, because a design that is sound against a static tree is unsound against a peer that can swap a path component mid-operation. **Ordering:** all checks capable of producing a security, policy, or confinement refusal MUST complete against the objects used for the commit before the destination write is committed; an implementation MUST NOT commit the destination and then report such a refusal. Temporary staging artifacts may still be created before that decision, subject to the identity-safe cleanup requirement above. + + **What this rules out, and what satisfies it.** Resolve-then-open-then-clean-up by path cannot establish these properties when the tree is concurrently mutable: an open can follow a swapped intermediate component and touch a location outside the root before any check runs, and cleanup performed by path can delete or modify an outside target through that same swapped path. Resolve descriptor-relative instead: root at a trusted directory descriptor, open each component relative to the preceding pinned descriptor, refuse symlinked components, and perform any cleanup relative to a held descriptor rather than by re-resolving a path. On Linux, `openat2` with appropriate `RESOLVE_*` controls is one implementation; other platforms need an equivalent mechanism, and where an untrusted principal can mutate the tree, filesystem permissions, mount-namespace isolation, or an OS sandbox is required in addition. Apply every decoding and normalization step (URL decoding, Unicode normalization, case folding, separator conversion, path cleanup) **before** constructing `RequestedPath`, and never transform the verified path afterward (§5.7.3). + + **Conformance.** A test for a refusal MUST distinguish adversary-created mutations from effects attributable to the refused operation. It MUST verify that no pre-existing object was changed by the refused operation, that the attempted payload was written nowhere by the operation, and that no artifact attributable to the operation remains, inside or outside the boundary. The test harness MUST account separately for mutations deliberately performed by the adversary. Asserting only that pre-existing objects survived and the payload is absent is insufficient, because it passes an implementation that leaves partial artifacts (for example, intermediate directories) behind on refusal. + + **Atomicity is a separate contract.** The no-effect-on-refusal property above covers *security, policy, and confinement* refusals reached before the write commits. An I/O failure *after* an authorized write has begun (a short write, a full disk, an interrupted syscall) is a different case: either promise atomic replacement (write the complete contents to a temporary file within the pinned destination directory, verify every byte was written, then install it with a descriptor-relative atomic rename) or state explicitly that partial writes are possible. Do not describe every failure as mutation-free unless atomic replacement is implemented. + + The cryptographic guarantee and the confinement guarantee are separate layers and should be reported separately: Ratify proves authorization for a canonical logical resource and path; the adapter attests context; the execution layer enforces filesystem confinement. ---