sandbox §5.3: an enforced grant field was outside the grant's content address #254

Merged
dcharlot merged 2 commits from dcharlot/bind-egress-credentials into main 2026-10-14 12:31:03 -04:00 AGit

2026-10-14

David Charlot
sandbox: drop an assertion of mine that could not fail
All checks were successful
lockfiles / every Cargo.lock is current (pull_request) Successful in 41s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 5s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 4s
no-vanity-metrics / No new orphan source files (pull_request) Successful in 5s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m58s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 3m23s
standards workspaces build + test / aac-rs (pull_request) Successful in 5m0s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m7s
standards workspaces build + test / map-rs (pull_request) Successful in 2m6s
standards workspaces build + test / mesh-rs (pull_request) Successful in 3m27s
tests in CI / every test file is run by a job or listed with its reason (pull_request) Successful in 1m20s
spec citations resolve / Code cites no section its SPEC lacks (pull_request) Successful in 5s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m10s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m3s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m33s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m15s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 4m15s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m15s
standards workspaces build + test / openpay (pull_request) Successful in 12m20s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m30s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m11s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m23s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m6s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 20m23s
Falsifying the test above found a defect in the test. It asserted that the
secret handle does not appear in the grant id:

    let core = format!("{:?}", grants_content_id(&live_key));
    assert!(!core.contains("live-key"), ...);

`grants_content_id` returns a BLAKE3 address. No formatting of a hash can
contain its own preimage, whatever went into it — so that assertion passes
against any implementation, including one that commits the handle verbatim. It
checked the hash function, not this code, which is the definition of
decoration.

I only noticed because mutation C — committing `endpoint|secret_ref`, which
really does put the handle in the preimage — was caught by a *different*
assertion (Bearer versus a named header, since that mutation also drops
`inject`). The one written for it stayed green, which is what a vacuous
assertion looks like from the outside.

Removed, with the reason in its place so it is not reintroduced, and a pointer
to the property that is testable: `commitment()` distinguishing its three
fields, which the swap and injection-mode assertions cover, and
`commitment_distinguishes_credentials_without_the_secret` in sandbox-capability
for the shape.

Re-falsified after the change: all three mutations still caught — the binding
disabled, the commitment reduced to the endpoint, and the handle committed
verbatim.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-14 10:05:40 -04:00
David Charlot
sandbox §5.3: an enforced grant field was outside the grant's content address
`grants_content_id` is the only thing that puts the grant set into a signed
attestation (`Attestation.grants_id`). It hand-builds a CBOR core of 27 keys
and covers `net_outbound` and `net_inbound` — and never
`g.network.credentials`.

Those credentials are enforced. `Enforcer::credential_for` returns the
credential the boundary injects on the unit's behalf for an authorised egress,
so *which* secret is injected is part of the capability surface. Swapping a
handle — `example/test-key` for `example/live-key` on an endpoint already in
`outbound` — changes nothing else about the grant, so the id was identical and
the mutation undetectable. Every attestation sealed under the old set went on
verifying while describing a surface no longer in force.

SPEC §1.2 requires the opposite in as many words: "a mutation to the granted
capability set MUST terminate the run and invalidate its attestation." The
repo already knew: a test beside this one is named
`io_ceiling_is_bound_into_grants_content_id` and opens "SPEC §1.2: an enforced
grant field MUST be part of the grant's content address, so loosening it
mid-run is detectable."

And the binding had been written. `EgressCredential::commitment()` documents
itself as "binds the `(endpoint, secret_ref, mode)` decision into an
attestation", and its only two callers in the tree were in its own test. The
claim in that doc was not true of the code. It is now — this gives it its
first real caller.

The commitment, never the credential, so the handle does not travel in the
grant id; the test asserts the handle is absent from it. `secret_ref` being a
name rather than a key is the whole safety property, and committing to the
credential itself would have spent it.

**Emitted only when the set is non-empty**, which is deliberate and not the
convention two keys above it. `opt_u64` always emits a presence-distinguishing
array because it must tell `None` from `Some(0)`; an empty credential set is
the same capability surface as a grant from before the field existed. So
omitting keeps every such grant's id byte-identical — including the
`b3:ONvIj2bsKw-2Kqe43mNV_3c0rrHTA3Df8xyM6D4ybnY` that `attestation.json` and
`negative.json` pin. All eight conformance classes pass with no skips. Only
grants that actually inject credentials get a new id, which is correct: theirs
was the id that was wrong.

SPEC §5.3 and spec/capabilities.md declared `NetworkGrant { outbound, inbound }`
— the enforced field was absent from the normative type as well as from the
address. Both now carry it, with the three rules: the handle is a name and
never a key, a credential's endpoint MUST also be in `outbound` (already
enforced by `injection_within_egress`), and the set MUST be in the content
address, by commitment.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-14 10:04:05 -04:00