diff --git a/docs/rfcs/composite-component-appraisal.md b/docs/rfcs/composite-component-appraisal.md index 05e96b22..c15ae3ac 100644 --- a/docs/rfcs/composite-component-appraisal.md +++ b/docs/rfcs/composite-component-appraisal.md @@ -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. diff --git a/tests/test_evidence_resolution_digest.py b/tests/test_evidence_resolution_digest.py new file mode 100644 index 00000000..1cd6b1f2 --- /dev/null +++ b/tests/test_evidence_resolution_digest.py @@ -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