Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 138 additions & 1 deletion docs/rfcs/composite-component-appraisal.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,147 @@ Both are now settled in the experimental text and pinned by vectors. The composi
1. **Component type registry and extension routing.** The ten categories are closed in this experiment, and an unknown one is rejected even on an optional component. Should a registry allow profile-defined subtypes, and should an unknown optional category be ignored instead of rejected?
2. **Relationship method registry.** Two methods, one relationship value. What does a third method need to register: a digest preimage, a check a second implementation can run, and vectors?
3. **Should `resolver` be required?** The requirement as first written said yes. This profile says no, because a required hint nobody fetches adds a field without adding a check. If evidence resolution is specified, the answer may change.
4. **Evidence resolution.** Who fetches, from where, with what retention, and what status a relying party reports when the fetch fails. The original design said `unverifiable`, not `contraindicated`, unless other evidence contradicts the claim. Nothing implements it yet.
4. **Evidence resolution.** Who fetches, from where, with what retention, and what status a relying party reports when the fetch fails. The original design said `unverifiable`, not `contraindicated`, unless other evidence contradicts the claim. Nothing implements it yet. The proposed bounded answer below specifies an opt-in offline slice; only its ER01 canonical-digest control is executed.
5. **Delegation chains and appraiser revocation.** Can an appraiser delegate further, and how does a relying party learn that an appraiser key was withdrawn before `valid_until`?
6. **MCP server components.** What evidence an `mcp-server` component rests on (artifact provenance, endpoint identity, transport key binding), and a relationship method that binds a catalog to the server that served it. For the catalog half, [`tool-catalog-observed-digest.md`](tool-catalog-observed-digest.md) states one derivation of a `tool-catalog` component's `observed_digest` from the served `tools/list`, with pinned bytes and `COMP-MCP-004` to `006`; it answers neither the server half nor the relationship method.
7. **Composite precedence and freshness (§8.3).** The experiment settles both: the token's `exp` bounds the composite, and `missing` outranks `unverifiable`. Both readings were defensible, and a reviewer may still invert either one. If so, the four vectors that pin them change with the text.
8. **Binding-only endpoints.** A binding endpoint with no entry in `requirements.components` is required but undeclared, so such a configuration can never affirm. Should requirement loading reject it?

The checks behind all of this can be run from a checkout: `node tools/independent-verifier/verify.mjs examples/verifier-token-conformance/vectors/*.json` prints one outcome per vector, and `python -m pytest tests/test_stage4_relationships.py` runs the worked example in §7 on the saved packets.


### 11.1 Proposed answer to question 4: offline evidence resolution

Informative proposal for [#441](https://github.com/agentrust-io/trace-spec/issues/441).
No resolver, wire field or appraisal outcome is implemented by this text.
Maintainers still select the profile and local comparison representation.
If this slice is adopted, section 9's first non-goal would change to reflect the
new resolution and re-appraisal scope.

#### Claim and trust boundary

The first slice resolves one saved Azure SNP execution packet from
operator-configured offline storage and independently re-appraises its execution
and collateral under declared inputs. It establishes what the second appraisal
finds about those bytes under that context. It cannot establish what the original
appraiser read, current platform freshness, or protected model execution.

Delegated appraisal remains a legitimate alternative trust model. RATS [R1]
does not require every relying party to parse raw evidence. This opt-in profile
is for consumers that explicitly require their own re-appraisal.

The resolver has no network authority from a signed hint. Its operator controls
storage, size limits, access and retention. Missing, denied, deleted, oversized
or unavailable evidence leaves the corresponding appraisal unestablished.
A digest mismatch is a binding failure, not proof that the asserted content is false.

#### Proposed processing obligations

1. The resolver retains the received bytes in the declared private store and parses unambiguously,
including duplicate-member refusal, before canonicalization.
2. The resolver checks the reference against the existing `canonical_digest(packet)` contract
in `prototype/verifier_token.py`: RFC 8785 canonical JSON and the named digest.
Whitespace differences alone do not change that object digest. Arbitrary
downloaded text hashes are not interchangeable with it [R2].
3. The relying party independently supplies holder/challenge context, execution policy, accepted
roots, platform collateral, TCB floors and evaluation instant.
4. The appraiser reuses the authenticated execution path before collateral appraisal.
`prototype/snp_collateral.py::appraise_collateral` explicitly assumes an
already-authenticated packet; calling it alone is insufficient.
5. The relying party retains execution and collateral results separately and compares eligible local
results with the original signed components without rewriting those components.
Shared evidence identity does not imply that both appraisals occurred.
6. The resolver preserves original observation/appraisal times. Fetching or serializing the
packet today does not create new freshness.

The existing historical packet can legitimately fall below the selected vendor
floor. Such a result is an important control, not a reason to lower a real
acceptance floor. Any synthetic affirmative fixture has an explicit test policy
and is never described as production admission evidence.

#### Proposed acceptance matrix

ER01 is executed by `tests/test_evidence_resolution_digest.py`; ER02 to ER09
remain planned. Each planned negative needs a valid twin reaching its specific
boundary, including genuine signatures for semantic-binding cases.

| ID | Controlled pair | Required distinction |
| --- | --- | --- |
| ER01 (executed) | Same synthetic object, changed whitespace, member order and number spelling / changed member; UTF-16 key order and exponent spelling controls | Same canonical digest / different digest, checked against a pinned canonical preimage |
| ER02 | Available packet / missing, denied or deleted packet | Appraisal attempted / unavailable evidence |
| ER03 | Authenticated expected packet / wrong AK or root | Eligible appraisal / authentication failure |
| ER04 | Correct holder and challenge / validly signed wrong context | Correct binding / no applicable appraisal |
| ER05 | Both selected appraisals / execution only | Two reported results / collateral unappraised |
| ER06 | Explicit synthetic accepted test policy / genuine below-floor packet | Scoped affirmative control / contraindicated platform |
| ER07 | Recorded historical instant / same bytes fetched later | Historical result / no renewed observation freshness |
| ER08 | Agreed local result / eligible contradictory local result | Agreement / explicit comparison difference |
| ER09 | Unambiguous JSON / duplicate member or exceeded size limit | Parseable evidence / refused resolution |

ER01 checks the existing helper only: three JSON encodings parse to the same
synthetic object and match the SHA-256 of explicitly pinned canonical bytes.
Changing one nested member changes the expected digest. Two additional cases pin
UTF-16 key ordering and exponent spelling, distinguishing JCS from sorted JSON
even if whole-valued floats are first normalized to integers. It neither resolves a
reference nor authenticates or appraises a platform packet. Run
`python -m pytest -q tests/test_evidence_resolution_digest.py`.

#### Corpus, second checker and evaluation plan

A future manifest records: case ID and version; source revision/file; exact
received-byte digest and canonical-object digest as distinct named values;
evidence class; trusted input files and digests; original observation and
evaluation instants; expected named results; positive twin; targeted mutation;
reproduction command; and public/private disposition. Real device identifiers
and protected payloads remain private. Predictable-content hashes are not
automatically safe to publish. Omitted raw material makes public reproduction
incomplete and is declared as such.

The bounded second checker validates resolution identity, invocation coverage
and result comparison without importing the reference adapter's decision logic.
Shared cryptographic libraries and specifications are disclosed dependencies;
this is not independent platform measurement. A separate qualified reader
defines expected cases before seeing checker outputs.

Compare signature/digest-only checking with the existing full appraisal path
and a strong stateful baseline given identical evidence and context. For change
handling, compare selective invalidation with full reassessment. Report false
establishment, legitimate controls retained, unavailable results and cost by
mechanism. Keep held-out mechanisms separate from template variants [R5].

Planned mutations disable digest, authentication, required-profile invocation
and result-comparison guards separately. The matching negative then fails a
behavioral assertion; valid twins still pass. Import errors and reject-all
behavior do not count as adequate detection. No benchmark result is claimed.

#### Adjacent decisions, not bundled implementation

- [#442](https://github.com/agentrust-io/trace-spec/issues/442): first decide
leaf-only appraiser status, authenticated key/grant identity, cache freshness,
effective revocation time and unavailable outcomes. Retain historical and
current-admission policies separately. A cached good response can entail a
bounded observation delay; immediate revocation does not follow [R3].
- [#440](https://github.com/agentrust-io/trace-spec/issues/440): separately select
authenticated admission-store transport, consistency and recovery assumptions.
Partition, concurrent consume, restart, promotion, rollback and clock-boundary
cases need history-based tests. A linearizable admission object does not make
an unrelated external effect exactly once [R4].

These dependencies cannot be marked complete by accepting this informative text.

#### Literature and its application

- R1: [RFC 9334, RATS Architecture (2023), sections 8 and 10](https://www.rfc-editor.org/rfc/rfc9334.html).
Conceptual messages, appraisal policies and freshness inform the trust boundary.
- R2: [RFC 8785, JSON Canonicalization Scheme (2020)](https://www.rfc-editor.org/rfc/rfc8785.html).
Defines the canonicalization antecedent; existing repository code determines
this packet's preimage contract.
- R3: [Samuel et al., Survivable Key Compromise, CCS 2010](https://theupdateframework.io/papers/survivable-key-compromise-ccs2010.pdf).
Timeliness and rollback are distinct trust-maintenance problems; not a TRACE
appraiser-status implementation or mandated delegation scheme.
- R4: [Herlihy and Wing, Linearizability, 1990](https://cs.brown.edu/people/mph/HerlihyW90/p463-herlihy.pdf).
Object-history reasoning motivates admission tests, not external-effect atomicity.
- R5: [Zabolotnii, Runtime Assurance Contracts, v2, 16 October 2026](https://arxiv.org/html/2609.39717v2).
Detector omissions and strong stateful comparisons constrain evaluation claims.

These sources are prior art, not proof of this proposal's correctness or novelty.
No standardization, hardware run, independent acceptance or issue closure is claimed.
42 changes: 42 additions & 0 deletions tests/test_evidence_resolution_digest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""ER01: object identity uses the existing JCS digest, not downloaded JSON bytes."""

import copy
import hashlib
import json

import pytest

from prototype.verifier_token import canonical_digest


@pytest.mark.parametrize("encoded", [
'{"packet":{"name":"synthetic","count":1},"version":1}',
'{\n "version": 1,\n "packet": { "count": 1, "name": "synthetic" }\n}',
'{"version":1.0,"packet":{"name":"synthetic","count":1.0}}',
], ids=["compact", "whitespace-and-order", "equivalent-numbers"])
def test_er01_canonical_identity_survives_encoding_but_not_member_change(encoded):
packet = json.loads(encoded)
# Pin the expected preimages without importing the production serializer.
canonical = b'{"packet":{"count":1,"name":"synthetic"},"version":1}'
changed_canonical = b'{"packet":{"count":2,"name":"synthetic"},"version":1}'
expected = "sha256:" + hashlib.sha256(canonical).hexdigest()
assert canonical_digest(packet) == expected
assert "sha256:" + hashlib.sha256(encoded.encode()).hexdigest() != expected

changed = copy.deepcopy(packet)
changed["packet"]["count"] = 2
assert canonical_digest(changed) == "sha256:" + hashlib.sha256(changed_canonical).hexdigest()
assert canonical_digest(changed) != expected
assert canonical_digest(packet) == expected


@pytest.mark.parametrize(("packet", "canonical"), [
({"\ue000": 1, "\U0001f600": 2}, '{"\U0001f600":2,"\ue000":1}'.encode()),
({"small": 1e-7}, b'{"small":1e-7}'),
], ids=["utf16-key-order", "exponent-spelling"])
def test_er01_pins_jcs_beyond_sorted_json(packet, canonical):
# Both cases distinguish JCS from sorted JSON even after integral-float normalization.
expected = "sha256:" + hashlib.sha256(canonical).hexdigest()
assert canonical_digest(packet) == expected
sorted_json = json.dumps(packet, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
assert "sha256:" + hashlib.sha256(sorted_json.encode()).hexdigest() != expected
Loading