spec: tighten ifc-fides framing and evidenceRef canonicalization (review @Rul1an)

ifc-fides:
- lead Label semantics with the load-bearing wire/host split (wire markers are
  advisory hints; reader-set semantics are host-resolved)
- state the join asymmetry as principled, not incidental: integrity join is
  total/wire-computable (small closed lattice); confidentiality join is partial
  because public is the only wire-computable case (reader set = top), while
  private join is the intersection the opaque markers don't carry
- frame wire opaqueness as a property of the security model, not a spec limitation
- add fail-closed handling when resolution is unavailable (ref absent, digest-only,
  source unreachable): never treat opaque labels as equal or as public; unknown/
  mixed provenance classifies private; resolved reader sets are not durable grants

trust-annotations:
- show jcs/rfc8785 alongside cbor/rfc8949 so canonicalization reads as a
  per-reference envelope choice rather than a default
- note type/digest/canonicalization as the local-re-derivation minimum

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sam Morrow 2026-06-16 00:34:46 +02:00
commit 4967a9eccc
No known key found for this signature in database
2 changed files with 47 additions and 13 deletions

View file

@ -86,12 +86,24 @@ be inlined by deployments that accept the wire cost) has the shape:
### Label semantics ### Label semantics
The load-bearing distinction is between the wire and the host: **wire markers are
advisory hints; reader-set semantics are host-resolved.** The asymmetry between
the two joins below follows from that one cut.
- **Join on accumulation.** As a session ingests labeled results, the context - **Join on accumulation.** As a session ingests labeled results, the context
label is the *join* of what it has seen: integrity degrades toward label is the *join* of what it has seen: integrity degrades toward
`untrusted`, confidentiality narrows toward the smallest permitted reader set. `untrusted`, confidentiality narrows toward the smallest permitted reader set.
The **integrity join is total and computable from the wire values alone** The two joins differ in *where* they can be computed, and the difference is
(`untrusted` dominates). The **confidentiality join is NOT computable from the principled rather than incidental:
opaque wire markers alone** — see [Reader-set resolution](#reader-set-resolution). - **Integrity join is total and wire-computable.** The integrity lattice is
small and closed (`trusted ⊑ untrusted`), so `untrusted` dominates and the
join needs nothing beyond the wire values.
- **Confidentiality join is partial and host-resolved.** Reader sets are open
and host-knowledge-dependent. `public` is the one wire-computable case,
because its reader set is universal (`⊤`): `public ⊔ anything = public`.
`private ⊔ private`, by contrast, is the *intersection* of two reader sets
that the opaque markers don't carry, so it is **not** computable from the
wire — see [Reader-set resolution](#reader-set-resolution).
- **Policy check before egress.** Before a write/egress tool call, the host - **Policy check before egress.** Before a write/egress tool call, the host
checks whether the current context label may flow to the call's target. When checks whether the current context label may flow to the call's target. When
a label is absent, the host falls back to its default (trusted-action) a label is absent, the host falls back to its default (trusted-action)
@ -103,13 +115,16 @@ be inlined by deployments that accept the wire cost) has the shape:
### Reader-set resolution ### Reader-set resolution
`"private"` is intentionally opaque on the wire. Two distinct `"private"` `"private"` is intentionally opaque on the wire — and that opaqueness is a
markers (e.g. file contents from two different private repositories) are **not property of the security model, not a limitation of the spec. A reader set is not
equal**, and their confidentiality join is **not** the same `"private"` token: transmissible without policy context, so the wire shape correctly declines to
data derived from both may flow only to principals who can read *both* sources — carry it. Two distinct `"private"` markers (e.g. file contents from two different
the intersection of their reader sets. The opaque marker cannot express this private repositories) are **not equal**, and their confidentiality join is **not**
intersection, so a host that needs to make a precise cross-source flow decision the same `"private"` token: data derived from both may flow only to principals who
MUST resolve each `"private"` marker to a concrete reader set before joining. can read *both* sources — the intersection of their reader sets. The opaque marker
cannot express this intersection, so a host that needs to make a precise
cross-source flow decision MUST resolve each `"private"` marker to a concrete
reader set before joining.
Resolution is a host-side concern, performed at policy-decision time: Resolution is a host-side concern, performed at policy-decision time:
@ -121,8 +136,19 @@ Resolution is a host-side concern, performed at policy-decision time:
3. The host computes the flow decision over the resolved sets (intersection for 3. The host computes the flow decision over the resolved sets (intersection for
a join of multiple private sources) and then discards them. a join of multiple private sources) and then discards them.
Keeping resolution host-side is what lets the wire stay at `public` / `private` **When resolution is unavailable** — the `ref` is absent, the label is
while still supporting precise multi-source decisions. digest-only, or the originating system is unreachable at decision time — the host
MUST NOT treat two opaque labels as equal, and MUST NOT treat `"private"` as
`"public"`. It denies, prompts, or applies its configured fail-closed policy. Two
`"private"` labels are equal only once resolution proves their sources are; until
a source is established, unknown or mixed provenance classifies as `"private"`,
never defaulted to `"public"` from a repository-level shortcut.
The resolved reader set is a decision-time read performed under the host's own
credentials. It is not a durable grant: a host SHOULD NOT cache it as one or
serialize it back into annotations or evidence unless a deployment explicitly opts
in. This keeps the wire free of user identities while still letting the host make a
precise decision when it holds source provenance plus its own credentials.
### Relationship to `trust-annotations` ### Relationship to `trust-annotations`
@ -164,3 +190,4 @@ the IFC label is the precise, host-checkable evidence behind them.
| ---------- | ------------------------------------------------------------ | | ---------- | ------------------------------------------------------------ |
| 2026-06-10 | Initial draft skeleton. Reframed from a top-level `ifc` extension to a `trust-annotations` `evidenceRef` profile (`ifc.fides.v1`). | | 2026-06-10 | Initial draft skeleton. Reframed from a top-level `ifc` extension to a `trust-annotations` `evidenceRef` profile (`ifc.fides.v1`). |
| 2026-06-15 | Confidentiality limited to `public` / `private` on the wire (dropped reader-list); added Reader-set resolution section; emitter classifies per resource, not per repository. (Review: @JoannaaKL.) | | 2026-06-15 | Confidentiality limited to `public` / `private` on the wire (dropped reader-list); added Reader-set resolution section; emitter classifies per resource, not per repository. (Review: @JoannaaKL.) |
| 2026-06-16 | Lead the semantics with the wire-hint / host-resolved split; state the integrity-total vs confidentiality-partial asymmetry as principled (`public` = `⊤` is wire-computable, `private ⊔ private` is not); add fail-closed handling when resolution is unavailable and a no-durable-grant rule for resolved sets. (Review: @Rul1an.) |

View file

@ -101,6 +101,12 @@ the SEP-1913 discussion):
This single slot subsumes the previously separate `attestationChainRef` / This single slot subsumes the previously separate `attestationChainRef` /
`policyDecisionRef` ideas — both become `type` values. `policyDecisionRef` ideas — both become `type` values.
`canonicalization` is per-reference precisely so different evidence producers can
be re-derived independently. `cbor/rfc8949` and `jcs/rfc8785` (JSON
Canonicalization Scheme) are both valid envelope choices — neither is the default,
and the `type`/`digest`/`canonicalization` triple is the minimum a client needs
for local re-derivation regardless of which is used.
### Coarse vs. rich classification (DataClass) ### Coarse vs. rich classification (DataClass)
SEP-1913 carried a four-level data classification SEP-1913 carried a four-level data classification
@ -114,7 +120,7 @@ is recovered as an `evidenceRef` profile:
"type": "data-class.v1", "type": "data-class.v1",
// record resolves to e.g. { "class": "highly_confidential", "regulatory": ["hipaa"] } // record resolves to e.g. { "class": "highly_confidential", "regulatory": ["hipaa"] }
"digest": "sha256:…", "digest": "sha256:…",
"canonicalization": "cbor/rfc8949" "canonicalization": "jcs/rfc8785" // a JSON-canonicalized record; CBOR is equally valid
} }
``` ```
@ -174,3 +180,4 @@ and `evidenceRef` is how that claim is made checkable.
| Date | Change | | Date | Change |
| ---------- | ------------------------------------------------------------- | | ---------- | ------------------------------------------------------------- |
| 2026-06-10 | Initial draft skeleton. Narrowed to `sensitive` + `untrusted` + `evidenceRef`; DataClass demoted to a profile; `requires_review` moved to `action-metadata`. | | 2026-06-10 | Initial draft skeleton. Narrowed to `sensitive` + `untrusted` + `evidenceRef`; DataClass demoted to a profile; `requires_review` moved to `action-metadata`. |
| 2026-06-16 | Show `jcs/rfc8785` alongside `cbor/rfc8949` so canonicalization reads as a per-reference envelope choice, not a default; note the `type`/`digest`/`canonicalization` triple as the local-re-derivation minimum. (Review: @Rul1an.) |