`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]>