diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb569ae..cddf64f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,11 +95,13 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR. ### Added +- **`observed-effect` is a registered `references` relation.** The four registered values cover what was asked, who approved, what the agent did and what a check found; none points at what actually changed. `observed-effect` is a signed record of the state change an observer outside the agent saw over one interval: the state root before and after, the paths observed and any part of them unseen, the digest of the authority change ran under, and a subject digest equal to the after-state root. `docs/references-registry.md` defines the referenced object as facts rather than a wire format and bounds what a relying party may establish to the object, as for `condition-appraisal`: an interval in which the observer and the observed party agree is not attested evidence that the agent's report was true, and one in which they disagree is not a finding against the record. `examples/observed-effect/` carries five signed records against an effect store whose two statements are copied byte for byte from the published observed-effect conformance corpus: verified, altered after issue, a disagreeing interval that verifies exactly as the agreeing one does, unresolvable, and an observer whose key the relying party does not hold; `tests/test_observed_effect_fixtures.py` recomputes every finding from the committed bytes and re-runs the generator against them. Each verdict names the state of the referenced observation (verified, digest mismatch, unresolved, unverified) and never the effect it reports, with resolution, digest match and signature verification kept as separate findings. Proposed on #402. Informative: no requirement, schema shape or wire format changes; the schema's `rel` description gains the name. + - **PIC/TRACE successor evidence is temporally separated from pre-execution authorization (#424, superseding the one-stage binding added in #340 for #338).** The signed pre-execution authorization no longer carries a digest of evidence that can exist only after execution. `intent_bridge.sign_successor_artifact()` creates a separate observer-authenticated post-execution artifact binding `authorization_id` to the exact successor-envelope digest, and `verify_successor_artifact()` checks the observer key, authorization link and envelope binding. `schema/pic-trace-successor-v1.json` defines the detached successor artifact while `schema/pic-trace-bridge-v1.json` remains the pre-execution authorization schema. This preserves the invariant that post-execution evidence cannot be a signed input to the artifact relied on as pre-execution authorization. Reported and implemented by @altrudev; #363 requires corresponding corpus regeneration before merge. - **`provenance.verify_record()` takes an explicit `now` (#411).** A non-negative integer Unix timestamp pins the freshness decision so it can be replayed. Booleans and other types are refused as configuration errors. Omitting it keeps the host clock. -- **`condition-appraisal` is a registered `references` relation, and the registry says what it is.** #191, #209, #220 and later #321 each asked for a way to bind a governance fact held outside the record, and #226 read them as one question about the `rel` registry of section 3.1.2. This settles the part ruled on there: the registry is informative and open, the verifier rules of section 3.1.2 stay normative and section 3.1.2 now says so below them, and `docs/references-registry.md` holds each registered value's referenced object, what a relying party may establish from a resolved one, and the steps by which a name is added. The fourth value, `condition-appraisal`, is an independent check's finding on whether a stated condition is established by a stated subject: a test run, a schema validation, a contract check. Its referenced object binds the condition and the subject by digest, names its issuer and key, carries the outcome in the issuer's own closed vocabulary, and signs the canonical bytes of the rest; the reference's `digest` is over the object as retained. What a relying party may establish is bounded to the object: that the cited bytes are the cited bytes and, where it holds the issuer's key, that the issuer signed them; neither reaches the record, a `pass` is not attested evidence that the condition held, and a `fail` is not a finding against the record. `examples/condition-appraisal/` carries five signed records from a seeded generator against an appraisal store: confirmed, altered after issue, a fail that verifies exactly as the pass does, unresolvable, and an issuer whose key the relying party does not hold; `tests/test_condition_appraisal_fixtures.py` recomputes every verdict from the committed bytes and re-runs the generator against them. Re-running a cited check is a separate proposal and no part of this change. Proposed by @nutstrut on #226; ruled on by @imran-siddique. Informative: no requirement, schema shape or wire format changes; the schema's `rel` description gains the name. +- **`condition-appraisal` is a registered `references` relation, and the registry says what it is.** #191, #209, #220 and later #321 each asked for a way to bind a governance fact held outside the record, and #226 read them as one question about the `rel` registry of section 3.1.2. This settles the part ruled on there: the registry is informative and open, the verifier rules of section 3.1.2 stay normative and section 3.1.2 now says so below them, and `docs/references-registry.md` holds each registered value's referenced object, what a relying party may establish from a resolved one, and the steps by which a name is added. The fourth value, `condition-appraisal`, is an independent check's finding on whether a stated condition is established by a stated subject: a test run, a schema validation, a contract check. Its referenced object binds the condition and the subject by digest, names its issuer and key, carries the outcome in the issuer's own closed vocabulary, and signs the canonical bytes of the rest; the reference's `digest` is over the object as retained. What a relying party may establish is bounded to the object: that the cited bytes are the cited bytes and, where it holds the issuer's key, that the issuer signed them; neither reaches the record, a `pass` is not attested evidence that the condition held, and a `fail` is not a finding against the record. `examples/condition-appraisal/` carries five signed records from a seeded generator against an appraisal store: verified, altered after issue, a fail that verifies exactly as the pass does, unresolvable, and an issuer whose key the relying party does not hold; `tests/test_condition_appraisal_fixtures.py` recomputes every verdict from the committed bytes and re-runs the generator against them. Re-running a cited check is a separate proposal and no part of this change. Proposed by @nutstrut on #226; ruled on by @imran-siddique. Informative: no requirement, schema shape or wire format changes; the schema's `rel` description gains the name. - **The schema, the reference model and the schema reference carry the reproducibility claim (section 3.1.4).** `schema/trace-claim.json` and its packaged copy gain the optional top-level `reproducibility` object: `function`, `code_identity`, an optional `code_resolver`, `input_closure` as `{id, digest, resolver}` entries with every member required, and `transcript_digest`. `appraisal` gains `method`, the closed set `re-execution`, and `re_execution`: `outcome` of `reproduced`, `diverged` or `not-attempted`, `observed_digest`, `reason`, and an optional `verifier_code_identity`. The three presence rules the section states are schema conditionals and model validators alike: `re_execution` is present exactly when `method` is `re-execution`, `observed_digest` is required on `diverged`, `reason` is required on `not-attempted`. `models.py` adds `Reproducibility`, `ClosureEntry` and `ReExecution`; `docs/schema.md` documents every member in its section. `examples/reproducibility-claim/` holds 21 signed vectors from a seeded key: five accepted records, a claim with no result and one result per outcome, and sixteen rejected ones, two per rule, with the transcript and closure digests recomputable from the JSON they are taken over. Tracks #364; closes #366. diff --git a/docs/references-registry.md b/docs/references-registry.md index 1aecdb34..c5430de3 100644 --- a/docs/references-registry.md +++ b/docs/references-registry.md @@ -43,6 +43,7 @@ schema description, in `docs/schema.md`, or in this document. | `approval-outcome` | An attributable human approval attached to a step-up or defer decision | §3.1.2; the CHAP mapping in [`crosswalks/chap-review-decisions.md`](crosswalks/chap-review-decisions.md) | [`examples/chap-approval-outcome/`](../examples/chap-approval-outcome/) | | `behavior-trace` | A behavioural record of what the agent did, of which this record is the environment evidence | §3.1.2 | none committed | | `condition-appraisal` | An independent check's finding on whether a stated condition is established by a stated subject | This document, below | [`examples/condition-appraisal/`](../examples/condition-appraisal/) | +| `observed-effect` | A signed record of the state change an observer outside the agent saw over one interval | This document, below | [`examples/observed-effect/`](../examples/observed-effect/) | The first three were registered by the change that introduced the block (#197) and their referenced objects are defined to the extent the documents cited define them. @@ -129,6 +130,75 @@ separate proposal from this registration. Nor is it compliance evidence: §3.1.2 says of the whole block that a record pointing at a check attests that it points there, and nothing about whether an obligation was met. +## `observed-effect` + +### Meaning + +The record points at what an observer the agent does not control saw change while the +agent ran: the state before an interval, the state after it, the paths the observation +covered, and the authority under which change was permitted. The four values registered +before it cover what was asked (`authorized-intent`), who approved (`approval-outcome`), +what the agent did (`behavior-trace`) and what a check found (`condition-appraisal`); +this is the question a relying party asks after all four, what actually changed. + +Two neighbours it is not. A `behavior-trace` is a record of the agent's actions, often +written by the agent or its host, and a state change can happen that no action in it +accounts for, or fail to happen where one claims it did; an `observed-effect` is taken +over the state itself, from outside the party it describes. And it is not a +`condition-appraisal`: it states what changed and within what scope, and it holds the +change against no criteria. A check over an observed interval, such as whether a write +fell outside the permitted paths, is a `condition-appraisal` whose subject is the +interval. + +### The referenced object + +A signed, content-addressed statement. The committed example uses an in-toto Statement +in a DSSE envelope whose `predicateType` is +`https://probityai.github.io/agent-evidence-vectors/predicate/v1/observed-effect`, and +the wire format is the issuer's; what registration fixes is the set of facts the object +carries and how the reference's `digest` is taken over it. + +| Fact | Carried as | Why it is required | +|---|---|---| +| The interval | The state root before, the state root after, and the times the interval opened and was sealed | Without both roots there is no change to point at, only a claim that one happened | +| The scope | The paths the observation covered, and any part of them it could not see | A change observed over part of a system says nothing about the rest; an unstated scope reads as total | +| The authority | A digest over the policy or grant under which change was permitted | Binds the change to the permission it ran under, so a later grant cannot be read back onto it | +| The observer | An identifier for the party that observed, and a reference to its verification key | §3.3.2's `issuer` and `issuer_key_id`, with the binding to a call replaced by the bindings above | +| The subject | A digest equal to the after-state root | Makes the object identify a state, not only describe one | +| The signature | The observer's signature over the object | §3.3.2 step 1, applied to this object; the committed example signs the DSSE pre-authentication encoding of the statement | + +The reference's `digest` is over the RFC 8785 canonical form of the complete object, +signature included, as the resolver retains it. `id` is the object's identifier within +the resolver's system and `resolver` is the party obliged to keep it resolvable, as for +every entry. + +The committed example, [`examples/observed-effect/`](../examples/observed-effect/), +resolves two statements copied byte for byte from a published conformance corpus for +that predicate type, so a second implementation already verifies the objects it cites. +A producer whose observer emits another shape that carries these facts is within this +definition. + +### What a verifier may conclude + +The same three findings as for `condition-appraisal`, each separable, all three +reported: + +1. **Whether the reference resolves.** If not, the observation is unresolved, which is a + different answer from "nothing changed", and under rule 3 of §3.1.2 it is never a + reason to reject the record. +2. **Whether the resolved bytes are the cited bytes.** A digest match establishes + identity; a mismatch is a finding about the store. +3. **Whether the object verifies under its named observer's key**, when the relying + party holds that key. An observer key the verifier does not hold makes the object + unverified, not invalid. + +All three are findings about the object and none reaches the record. What the object +reports is carried as the observer stated it and promotes in neither direction: an +interval in which the observer and the observed party agree is not attested evidence +that the agent's report was true, and one in which they disagree is not a finding +against the record. The example set holds an agreeing and a disagreeing interval side +by side, and the two records verify identically. + ## Adding a name A registration is a non-breaking spec change of the informative kind under diff --git a/docs/schema.md b/docs/schema.md index 0453c9e7..594134ab 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -129,13 +129,13 @@ A record whose `kind` is not `self` **must** carry `runtime.platform: "software- ## `references` {#trace-field-references} -An array of pointers to facts held outside this record: an authorization decided before execution, a human approval, a behavioural trace, an independent check's finding. What the signature attests is that this record points there, not the truth of what it points at. +An array of pointers to facts held outside this record: an authorization decided before execution, a human approval, a behavioural trace, an independent check's finding, an observed change of state. What the signature attests is that this record points there, not the truth of what it points at. `origin` records where evidence *came from* and can lower assurance. `references` records what a record *points at* and cannot. Before the block existed, a record that needed to name something external had to use `origin` and take `runtime.platform: "software-only"` with it, which said something untrue about how the evidence was obtained. | Field | Type | Required | Description | |---|---|---|---| -| `rel` | string | **yes** | Registered values: `authorized-intent`, `approval-outcome`, `behavior-trace`, `condition-appraisal`. A registry rather than a closed set, so the schema does not restrict which relation is named: only that one is: the value must be non-empty | +| `rel` | string | **yes** | Registered values: `authorized-intent`, `approval-outcome`, `behavior-trace`, `condition-appraisal`, `observed-effect`. A registry rather than a closed set, so the schema does not restrict which relation is named: only that one is: the value must be non-empty | | `id` | string | **yes** | Identifier of the referenced fact within the resolver's system | | `resolver` | string | **yes** | Identifier of the party obliged to resolve `id` | | `retention` | string | no | ISO 8601 duration the resolver undertakes to keep `id` resolvable. An undertaking only; nothing enforces it | diff --git a/examples/README.md b/examples/README.md index 91223820..ec3c0789 100644 --- a/examples/README.md +++ b/examples/README.md @@ -45,10 +45,16 @@ checking any archived record, including these. See that directory's README. - `condition-appraisal/`: five signed Trust Records, each citing an independent check's finding through a `references` entry with `rel: "condition-appraisal"`, against an - appraisal store committed beside them: confirmed, altered after issue, a fail that + appraisal store committed beside them: verified, altered after issue, a fail that verifies exactly as the pass does, unresolvable, and an issuer whose key the relying party does not hold. Every digest recomputes; see that directory's README and `docs/references-registry.md`. +- `observed-effect/`: five signed Trust Records, each citing an observed mutation + interval through a `references` entry with `rel: "observed-effect"`, against an effect + store committed beside them: verified, altered after issue, an interval where the + observer and the observed party disagree that verifies exactly as the agreeing one + does, unresolvable, and an observer whose key the relying party does not hold. Every + digest recomputes; see that directory's README and `docs/references-registry.md`. - `signature-encoding/`: three signed Trust Records that separate a canonically-encoded embedded signature from two non-canonical respellings of the same 64 bytes. Proposed, not accepted normative text (#247); carries a `scan_published_signatures.py` tool diff --git a/examples/condition-appraisal/01-appraisal-confirmed.json b/examples/condition-appraisal/01-appraisal-verified.json similarity index 100% rename from examples/condition-appraisal/01-appraisal-confirmed.json rename to examples/condition-appraisal/01-appraisal-verified.json diff --git a/examples/condition-appraisal/README.md b/examples/condition-appraisal/README.md index b58e016d..0f40dda6 100644 --- a/examples/condition-appraisal/README.md +++ b/examples/condition-appraisal/README.md @@ -41,19 +41,26 @@ and re-runs the generator against the committed files. ## Fixture cases Expected results are machine-readable in [`expected.json`](expected.json). Every record -verifies as a TRACE record; what differs is what the reference resolves to. +verifies as a TRACE record; what differs is what the reference resolves to. Resolution, +digest match and signature verification are three separate findings, each reported in +its own column and its own field. The verdict names the state of the referenced +appraisal, not the condition it reports on: a verified appraisal does not establish +that the condition held, and a digest mismatch does not establish that it did not. | Record | Store | Reference | Digest | Issuer key held | Issuer signature | Outcome | Verdict | |---|---|---|---|---|---|---|---| -| `01-appraisal-confirmed.json` | `appraisal-store.json` | resolves | matches | yes | verifies | `pass` | appraisal confirmed | -| `02-appraisal-altered-after-issue.json` | `appraisal-store-altered.json` | resolves | **differs** | yes | **fails** | `pass` | appraisal contradicted | -| `03-outcome-is-a-fail.json` | `appraisal-store.json` | resolves | matches | yes | verifies | **`fail`** | appraisal confirmed | -| `04-reference-unresolvable.json` | `appraisal-store.json` | **no such entry** | n/a | n/a | n/a | n/a | appraisal unconfirmed | +| `01-appraisal-verified.json` | `appraisal-store.json` | resolves | matches | yes | verifies | `pass` | appraisal verified | +| `02-appraisal-altered-after-issue.json` | `appraisal-store-altered.json` | resolves | **differs** | yes | **fails** | `pass` | appraisal digest mismatch | +| `03-outcome-is-a-fail.json` | `appraisal-store.json` | resolves | matches | yes | verifies | **`fail`** | appraisal verified | +| `04-reference-unresolvable.json` | `appraisal-store.json` | **no such entry** | n/a | n/a | n/a | n/a | appraisal unresolved | | `05-issuer-key-not-configured.json` | `appraisal-store.json` | resolves | matches | **no** | not checked | `pass` | appraisal unverified | `02` is `appraisal/2`, issued as a `fail`, rewritten in the stored copy as a `pass` after the record was issued. Both checks catch it independently: the record's digest no longer matches, and the issuer's signature no longer verifies over the rewritten bytes. +Both findings are about the stored copy; neither says whether the deliverable met the +condition. A signature that fails over bytes whose digest does match is reported as +appraisal signature invalid, a separate verdict the tests exercise. `03` cites the same `appraisal/2` from the unaltered store. The finding is a `fail`, the record verifies exactly as `01` does, and the relying party reports the outcome without @@ -61,7 +68,7 @@ promoting it in either direction. `04` is what §3.1.2 rule 3 requires: a verifier must not reject a record because a reference cannot be resolved. The record verifies, and the appraisal it points at is -reported as unconfirmed, which is a different answer from "no appraisal". +reported as unresolved, which is a different answer from "no appraisal". `05` cites an appraisal signed by an issuer whose key this relying party does not hold. The rule §3.3.2 gives receipts applies: unverified, not invalid. The cited bytes are diff --git a/examples/condition-appraisal/expected.json b/examples/condition-appraisal/expected.json index 7de13643..66615715 100644 --- a/examples/condition-appraisal/expected.json +++ b/examples/condition-appraisal/expected.json @@ -13,7 +13,7 @@ } }, "cases": { - "01-appraisal-confirmed.json": { + "01-appraisal-verified.json": { "store": "appraisal-store.json", "trace_record_verifies": true, "reference_resolves": true, @@ -21,7 +21,7 @@ "issuer_key_configured": true, "appraisal_verifies": true, "outcome": "pass", - "verdict": "appraisal-confirmed" + "verdict": "appraisal-verified" }, "02-appraisal-altered-after-issue.json": { "store": "appraisal-store-altered.json", @@ -31,7 +31,7 @@ "issuer_key_configured": true, "appraisal_verifies": false, "outcome": "pass", - "verdict": "appraisal-contradicted" + "verdict": "appraisal-digest-mismatch" }, "03-outcome-is-a-fail.json": { "store": "appraisal-store.json", @@ -41,7 +41,7 @@ "issuer_key_configured": true, "appraisal_verifies": true, "outcome": "fail", - "verdict": "appraisal-confirmed" + "verdict": "appraisal-verified" }, "04-reference-unresolvable.json": { "store": "appraisal-store.json", @@ -51,7 +51,7 @@ "issuer_key_configured": null, "appraisal_verifies": null, "outcome": null, - "verdict": "appraisal-unconfirmed" + "verdict": "appraisal-unresolved" }, "05-issuer-key-not-configured.json": { "store": "appraisal-store.json", diff --git a/examples/condition-appraisal/gen_condition_appraisal_vectors.py b/examples/condition-appraisal/gen_condition_appraisal_vectors.py index cd1710cb..f074d67a 100644 --- a/examples/condition-appraisal/gen_condition_appraisal_vectors.py +++ b/examples/condition-appraisal/gen_condition_appraisal_vectors.py @@ -146,7 +146,7 @@ def build() -> dict[str, Any]: cite = lambda id_: reference(id_, jcs_sha256(store["appraisals"][id_])) # noqa: E731 records = { - "01-appraisal-confirmed.json": record(producer, cite("appraisal/1")), + "01-appraisal-verified.json": record(producer, cite("appraisal/1")), "02-appraisal-altered-after-issue.json": record(producer, cite("appraisal/2")), "03-outcome-is-a-fail.json": record(producer, cite("appraisal/2")), "04-reference-unresolvable.json": record( @@ -164,25 +164,25 @@ def build() -> dict[str, Any]: # verifier is unverified, not invalid. "issuer_keys": {thumbprint(issuer_jwk): issuer_jwk}, "cases": { - "01-appraisal-confirmed.json": { + "01-appraisal-verified.json": { "store": "appraisal-store.json", "trace_record_verifies": True, "reference_resolves": True, "digest_matches": True, "issuer_key_configured": True, - "appraisal_verifies": True, "outcome": "pass", "verdict": "appraisal-confirmed", + "appraisal_verifies": True, "outcome": "pass", "verdict": "appraisal-verified", }, "02-appraisal-altered-after-issue.json": { "store": "appraisal-store-altered.json", "trace_record_verifies": True, "reference_resolves": True, "digest_matches": False, "issuer_key_configured": True, - "appraisal_verifies": False, "outcome": "pass", "verdict": "appraisal-contradicted", + "appraisal_verifies": False, "outcome": "pass", "verdict": "appraisal-digest-mismatch", }, "03-outcome-is-a-fail.json": { "store": "appraisal-store.json", "trace_record_verifies": True, "reference_resolves": True, "digest_matches": True, "issuer_key_configured": True, - "appraisal_verifies": True, "outcome": "fail", "verdict": "appraisal-confirmed", + "appraisal_verifies": True, "outcome": "fail", "verdict": "appraisal-verified", }, "04-reference-unresolvable.json": { "store": "appraisal-store.json", "trace_record_verifies": True, "reference_resolves": False, "digest_matches": None, "issuer_key_configured": None, - "appraisal_verifies": None, "outcome": None, "verdict": "appraisal-unconfirmed", + "appraisal_verifies": None, "outcome": None, "verdict": "appraisal-unresolved", }, "05-issuer-key-not-configured.json": { "store": "appraisal-store.json", "trace_record_verifies": True, diff --git a/examples/observed-effect/01-observation-verified.json b/examples/observed-effect/01-observation-verified.json new file mode 100644 index 00000000..a65d241f --- /dev/null +++ b/examples/observed-effect/01-observation-verified.json @@ -0,0 +1,43 @@ +{ + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1789776010, + "subject": "spiffe://trust.example.org/agent/build-bot", + "model": { + "provider": "example", + "model_id": "example-model" + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + }, + "policy": { + "bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 1, + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example.org" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "ysSfGiHZFwj77FqkuI2D7bc9K0sNHVbPcoDjroN3aeA" + } + }, + "references": [ + { + "rel": "observed-effect", + "id": "interval/1", + "resolver": "https://observer.example.org/intervals", + "digest": "sha256:00bd35730a2e8fa462a5fbf0a30aac71302fdbd02b5217d6c3950a66531a9121", + "retention": "P1Y" + } + ], + "signature": "QieV5TjRrAypQcw-2fNAUxvtnYj2PKBH54SjK2WOlP4fF-4AwbuGtwnRar9WUsdZgcAR5TlWtSSsuYFN_WK8Ag" +} diff --git a/examples/observed-effect/02-observation-altered-after-issue.json b/examples/observed-effect/02-observation-altered-after-issue.json new file mode 100644 index 00000000..40bfd35c --- /dev/null +++ b/examples/observed-effect/02-observation-altered-after-issue.json @@ -0,0 +1,43 @@ +{ + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1789776010, + "subject": "spiffe://trust.example.org/agent/build-bot", + "model": { + "provider": "example", + "model_id": "example-model" + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + }, + "policy": { + "bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 1, + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example.org" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "ysSfGiHZFwj77FqkuI2D7bc9K0sNHVbPcoDjroN3aeA" + } + }, + "references": [ + { + "rel": "observed-effect", + "id": "interval/2", + "resolver": "https://observer.example.org/intervals", + "digest": "sha256:69aab598cc1ea76cce6325742406dc7db49640527d686348a9e73353894dd3f8", + "retention": "P1Y" + } + ], + "signature": "hgIkXMRuWkIzc3DS-da6CfurMNDQtoxLaTSYoeZii3gOTdPUaWG0b3Cr2U-9UckF7ChOxwXWwMXzCyMKAqHABA" +} diff --git a/examples/observed-effect/03-observer-and-observed-disagree.json b/examples/observed-effect/03-observer-and-observed-disagree.json new file mode 100644 index 00000000..40bfd35c --- /dev/null +++ b/examples/observed-effect/03-observer-and-observed-disagree.json @@ -0,0 +1,43 @@ +{ + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1789776010, + "subject": "spiffe://trust.example.org/agent/build-bot", + "model": { + "provider": "example", + "model_id": "example-model" + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + }, + "policy": { + "bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 1, + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example.org" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "ysSfGiHZFwj77FqkuI2D7bc9K0sNHVbPcoDjroN3aeA" + } + }, + "references": [ + { + "rel": "observed-effect", + "id": "interval/2", + "resolver": "https://observer.example.org/intervals", + "digest": "sha256:69aab598cc1ea76cce6325742406dc7db49640527d686348a9e73353894dd3f8", + "retention": "P1Y" + } + ], + "signature": "hgIkXMRuWkIzc3DS-da6CfurMNDQtoxLaTSYoeZii3gOTdPUaWG0b3Cr2U-9UckF7ChOxwXWwMXzCyMKAqHABA" +} diff --git a/examples/observed-effect/04-reference-unresolvable.json b/examples/observed-effect/04-reference-unresolvable.json new file mode 100644 index 00000000..d17b9a3b --- /dev/null +++ b/examples/observed-effect/04-reference-unresolvable.json @@ -0,0 +1,43 @@ +{ + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1789776010, + "subject": "spiffe://trust.example.org/agent/build-bot", + "model": { + "provider": "example", + "model_id": "example-model" + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + }, + "policy": { + "bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 1, + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example.org" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "ysSfGiHZFwj77FqkuI2D7bc9K0sNHVbPcoDjroN3aeA" + } + }, + "references": [ + { + "rel": "observed-effect", + "id": "interval/9", + "resolver": "https://observer.example.org/intervals", + "digest": "sha256:5bff9cfbe71b95959bf6a22101688acb936c00323399089f0cb9b93255fbc874", + "retention": "P1Y" + } + ], + "signature": "JAOx0cH1jIw0KBUrArsmOT-Evlx0dgWQzsJgi-on-0wfGLLqXgSVGWpsg-Z1e53eyHJi2szlp0OeThLrPlFgDQ" +} diff --git a/examples/observed-effect/05-observer-key-not-configured.json b/examples/observed-effect/05-observer-key-not-configured.json new file mode 100644 index 00000000..2a37d771 --- /dev/null +++ b/examples/observed-effect/05-observer-key-not-configured.json @@ -0,0 +1,43 @@ +{ + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1789776010, + "subject": "spiffe://trust.example.org/agent/build-bot", + "model": { + "provider": "example", + "model_id": "example-model" + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + }, + "policy": { + "bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 1, + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example.org" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "ysSfGiHZFwj77FqkuI2D7bc9K0sNHVbPcoDjroN3aeA" + } + }, + "references": [ + { + "rel": "observed-effect", + "id": "interval/3", + "resolver": "https://observer.example.org/intervals", + "digest": "sha256:dcfe876e7184675bd9877675a6841292c3802d150b8c5550041be2e05cdf8f13", + "retention": "P1Y" + } + ], + "signature": "flpGZJRQ9Hf_EtszJyklnCwhe_bZaqauIbQoO3MRQ4OY6W_SBVdPzvsoGACDLWHQhhJgIDNfCMM5hgdA0Wy4BQ" +} diff --git a/examples/observed-effect/README.md b/examples/observed-effect/README.md new file mode 100644 index 00000000..547f7217 --- /dev/null +++ b/examples/observed-effect/README.md @@ -0,0 +1,98 @@ +# An observed mutation interval as an `observed-effect` reference + +A TRACE Trust Record can point at what an observer outside the agent saw change while +the agent ran: the state before an interval, the state after it, the paths the +observation covered, and the authority change ran under. These fixtures show that +composition. Each record carries one `references` entry with `rel: "observed-effect"`, +and its `digest` is the SHA-256 of the RFC 8785 canonical form of the DSSE envelope as +the resolver retains it. + +The relation is defined in [`docs/references-registry.md`](../../docs/references-registry.md). +What a relying party may establish from a resolved reference is bounded to the object: +that the resolved bytes are the cited bytes, and, under an observer key it holds, that +the named observer signed them. Neither reaches the record: under §3.1.2 rule 3 it +verifies the same whether the reference resolves or not, and nothing in the object +becomes attested evidence. An interval in which the observer and the observed party +agree is not attested evidence that the agent's report was true, and one in which they +disagree is not a finding against the record. Cases `01` and `03` are the same record +shape citing an agreeing and a disagreeing interval, and they verify identically. + +## Where the objects come from + +The two statements in [`source/`](source/) are copied byte for byte from the published +observed-effect conformance corpus, [`vectors-observed-effect/` at `b916489`](https://github.com/probityai/agent-evidence-vectors/tree/b91648940b2042ff1cc34d11ed2ac1d97b3e42d4/vectors-observed-effect), where each is a member that corpus's own verifier +accepts: `v1c6fdd82db5229e4` is an authoritative interval in which both sides agree, and +`v620e7755ba36aa0a` is one in which they disagree on the write count and the after-state +root. A member's identifier is the first 16 hex characters of SHA-256 over its file, so +a copy that drifted fails the generator. The corpus publishes the observer's public key +in its manifest, and that key is the one this relying party holds. + +Everything else derives from one published seed through +[`gen_observed_effect_vectors.py`](gen_observed_effect_vectors.py): the Trust Record +producer key, a second observer key that re-signs the agreeing statement for case `05`, +and the altered store. The set regenerates byte for byte. +[`tests/test_observed_effect_fixtures.py`](../../tests/test_observed_effect_fixtures.py) +recomputes every verdict from the committed bytes rather than reading it from +`expected.json`, and re-runs the generator against the committed files. + +## The referenced object + +`effect-store.json` holds three DSSE envelopes by identifier. Each one carries an in-toto +Statement whose `predicateType` is +`https://probityai.github.io/agent-evidence-vectors/predicate/v1/observed-effect`: + +| Member | What it is | +|---|---| +| `predicate.interval` | `beforeRoot`, `afterRoot`, `openedAt`, `sealedAt` | +| `predicate.pathScope`, `predicate.observation.coverage` | The paths observed, and whether any part of them went unseen | +| `predicate.authorityDigest` | The digest of the grant under which change was permitted | +| `predicate.dualValues` | Facts both sides report, with both values and whether they agree | +| `subject[0].digest.sha256` | Equal to `afterRoot` | +| `signatures[0]` | Ed25519 over the DSSE pre-authentication encoding, under the observer's `keyid` | + +This example checks the envelope, not the predicate's own rules. Those belong to the +predicate's conformance corpus, which is where both source statements come from. + +## Fixture cases + +Expected results are machine-readable in [`expected.json`](expected.json). Every record +verifies as a TRACE record; what differs is what the reference resolves to. Resolution, +digest match and signature verification are three separate findings, each reported in +its own column and its own field. The verdict names the state of the referenced +observation, not the effect it reports: a verified observation does not establish that +the reported change occurred, and a digest mismatch does not establish that it did not. + +| Record | Store | Reference | Digest | Observer key held | Envelope signature | Dual values | Verdict | +|---|---|---|---|---|---|---|---| +| `01-observation-verified.json` | `effect-store.json` | resolves | matches | yes | verifies | agree | observation verified | +| `02-observation-altered-after-issue.json` | `effect-store-altered.json` | resolves | **differs** | yes | **fails** | agree | observation digest mismatch | +| `03-observer-and-observed-disagree.json` | `effect-store.json` | resolves | matches | yes | verifies | **disagree** | observation verified | +| `04-reference-unresolvable.json` | `effect-store.json` | **no such entry** | n/a | n/a | n/a | n/a | observation unresolved | +| `05-observer-key-not-configured.json` | `effect-store.json` | resolves | matches | **no** | not checked | agree | observation unverified | + +`02` is `interval/2`, issued with two disagreeing rows, rewritten in the stored copy so +that both rows agree with the observed party's report, with the signature left as +issued. Both checks catch it independently: the record's digest no longer matches, and +the observer's signature no longer verifies over the rewritten payload. Both findings +are about the stored copy; neither says anything about whether the interval's change +happened. + +`03` cites the same `interval/2` from the unaltered store. The observer and the observed +party disagree, the record verifies exactly as `01` does, and the relying party reports +the disagreement without promoting it in either direction. + +`04` is what §3.1.2 rule 3 requires: a verifier must not reject a record because a +reference cannot be resolved. The record verifies, and the observation it points at is +reported as unresolved, which is a different answer from "nothing changed". + +`05` cites the agreeing statement re-signed by an observer whose key this relying party +does not hold. The rule §3.3.2 gives receipts applies: unverified, not invalid. + +## Running the checks + +``` +python -m pytest tests/test_observed_effect_fixtures.py +``` + +To regenerate, run the generator with no arguments. It is deterministic, so the +committed files only change when the generator or a source statement does. diff --git a/examples/observed-effect/effect-store-altered.json b/examples/observed-effect/effect-store-altered.json new file mode 100644 index 00000000..7ce1a166 --- /dev/null +++ b/examples/observed-effect/effect-store-altered.json @@ -0,0 +1,35 @@ +{ + "resolver": "https://observer.example.org/intervals", + "effects": { + "interval/1": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMiJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwMSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiM2NiMDJjODk3ZWViN2VhY2JiNTJmYzdmOTJkODcwZTk2NzgxYTRjN2VjNmMzYTljMDNhMGNmN2VlZDQ2YzQzMyIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiMmQxNmFmOGU2ZWZjY2RiYjFkOWFjZjA3MTAzNWVhNWUyNDJhY2JiMTY2NjNhM2QwNTI2MWEwNmNmOTkyZjljZGFhY2IyM2UzM2I5MDQ0ZjFiNGUyZWMyNDQ0ZGY4ZjQzNTI0MDI1MWUwNmNiMmE3MTI2ZWRhNGU4ZjJkZGI3MDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDEifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "f3cb9b6750737ef6789a72b71d8305f2", + "sig": "rW8A8so/7gwe3AcKwNkq1qSxKLjNETKKhOdJIyUSvo0pJrd8VGhtiI3AT6pWV+3yz4FL7Z6zvbqbHQp2gC6OAQ==" + } + ] + }, + "interval/2": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjEiLCJyZXBvcnRlZFZhbHVlIjoiMSJ9LHsiYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0IjoiaW50ZXJ2YWwuYWZ0ZXJSb290Iiwib2JzZXJ2ZWRWYWx1ZSI6ImM0MDkxOWNkYjhlNjZjOTM1ZDM5MDczNTI0MzZiOWU2ZmU3OTYyNzg5NTVhZDY2Y2JmYzFkOTVmN2FmOTk2ZjEiLCJyZXBvcnRlZFZhbHVlIjoiYzQwOTE5Y2RiOGU2NmM5MzVkMzkwNzM1MjQzNmI5ZTZmZTc5NjI3ODk1NWFkNjZjYmZjMWQ5NWY3YWY5OTZmMSJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwNSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiYmM5OTc5OGFmYmYxYTRhY2Y2MGUyOGM4OTEzYmQ0NjJlNTU1ODk5YzAyZGJkMTBiZGVkYWQzMzViZjRjYzBjYiIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiNzI4ZmJhN2ViZjJkY2RlMzVhNTk2YWEzOGQ3NTZiNjIwM2Y2ZDIxM2JkYThmMzM3NzUyYzRkZTBmYjdhNTZkM2I2ZDQ2ZDlkOTJhMTVlMzY3YzNhMDk1MTczNGVhZjc4MDYzYTNiN2U5OTc5OGE2ODIyMjI3NGM0NWZlMTdkMDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDUifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "f3cb9b6750737ef6789a72b71d8305f2", + "sig": "4MbIcwmjIjbpak6tDi+F1mlfLu8AV28k0Zg6+x/C8tXax2emMFWuEX8uQ0sXISgiW2UQn9DyxecijLsgRcXrDA==" + } + ] + }, + "interval/3": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMiJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwMSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiM2NiMDJjODk3ZWViN2VhY2JiNTJmYzdmOTJkODcwZTk2NzgxYTRjN2VjNmMzYTljMDNhMGNmN2VlZDQ2YzQzMyIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiMmQxNmFmOGU2ZWZjY2RiYjFkOWFjZjA3MTAzNWVhNWUyNDJhY2JiMTY2NjNhM2QwNTI2MWEwNmNmOTkyZjljZGFhY2IyM2UzM2I5MDQ0ZjFiNGUyZWMyNDQ0ZGY4ZjQzNTI0MDI1MWUwNmNiMmE3MTI2ZWRhNGU4ZjJkZGI3MDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDEifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "0f345495d0bdf85dcfda52b921aa265d", + "sig": "bkfApm1Zu5VvVdiJn2uMfiyJ/gfmdpnkHgmxPtI2j3kSrw+Ck5XVRHji4/Kc0vBdkldxuvwnMXPFBX9YpfvACQ==" + } + ] + } + } +} diff --git a/examples/observed-effect/effect-store.json b/examples/observed-effect/effect-store.json new file mode 100644 index 00000000..319dba95 --- /dev/null +++ b/examples/observed-effect/effect-store.json @@ -0,0 +1,35 @@ +{ + "resolver": "https://observer.example.org/intervals", + "effects": { + "interval/1": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMiJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwMSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiM2NiMDJjODk3ZWViN2VhY2JiNTJmYzdmOTJkODcwZTk2NzgxYTRjN2VjNmMzYTljMDNhMGNmN2VlZDQ2YzQzMyIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiMmQxNmFmOGU2ZWZjY2RiYjFkOWFjZjA3MTAzNWVhNWUyNDJhY2JiMTY2NjNhM2QwNTI2MWEwNmNmOTkyZjljZGFhY2IyM2UzM2I5MDQ0ZjFiNGUyZWMyNDQ0ZGY4ZjQzNTI0MDI1MWUwNmNiMmE3MTI2ZWRhNGU4ZjJkZGI3MDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDEifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "f3cb9b6750737ef6789a72b71d8305f2", + "sig": "rW8A8so/7gwe3AcKwNkq1qSxKLjNETKKhOdJIyUSvo0pJrd8VGhtiI3AT6pWV+3yz4FL7Z6zvbqbHQp2gC6OAQ==" + } + ] + }, + "interval/2": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiZGlzYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMSJ9LHsiYWdyZWVtZW50IjoiZGlzYWdyZWUiLCJmYWN0IjoiaW50ZXJ2YWwuYWZ0ZXJSb290Iiwib2JzZXJ2ZWRWYWx1ZSI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJyZXBvcnRlZFZhbHVlIjoiYzQwOTE5Y2RiOGU2NmM5MzVkMzkwNzM1MjQzNmI5ZTZmZTc5NjI3ODk1NWFkNjZjYmZjMWQ5NWY3YWY5OTZmMSJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwNSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiYmM5OTc5OGFmYmYxYTRhY2Y2MGUyOGM4OTEzYmQ0NjJlNTU1ODk5YzAyZGJkMTBiZGVkYWQzMzViZjRjYzBjYiIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiNzI4ZmJhN2ViZjJkY2RlMzVhNTk2YWEzOGQ3NTZiNjIwM2Y2ZDIxM2JkYThmMzM3NzUyYzRkZTBmYjdhNTZkM2I2ZDQ2ZDlkOTJhMTVlMzY3YzNhMDk1MTczNGVhZjc4MDYzYTNiN2U5OTc5OGE2ODIyMjI3NGM0NWZlMTdkMDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDUifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "f3cb9b6750737ef6789a72b71d8305f2", + "sig": "4MbIcwmjIjbpak6tDi+F1mlfLu8AV28k0Zg6+x/C8tXax2emMFWuEX8uQ0sXISgiW2UQn9DyxecijLsgRcXrDA==" + } + ] + }, + "interval/3": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMiJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwMSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiM2NiMDJjODk3ZWViN2VhY2JiNTJmYzdmOTJkODcwZTk2NzgxYTRjN2VjNmMzYTljMDNhMGNmN2VlZDQ2YzQzMyIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiMmQxNmFmOGU2ZWZjY2RiYjFkOWFjZjA3MTAzNWVhNWUyNDJhY2JiMTY2NjNhM2QwNTI2MWEwNmNmOTkyZjljZGFhY2IyM2UzM2I5MDQ0ZjFiNGUyZWMyNDQ0ZGY4ZjQzNTI0MDI1MWUwNmNiMmE3MTI2ZWRhNGU4ZjJkZGI3MDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDEifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "0f345495d0bdf85dcfda52b921aa265d", + "sig": "bkfApm1Zu5VvVdiJn2uMfiyJ/gfmdpnkHgmxPtI2j3kSrw+Ck5XVRHji4/Kc0vBdkldxuvwnMXPFBX9YpfvACQ==" + } + ] + } + } +} diff --git a/examples/observed-effect/expected.json b/examples/observed-effect/expected.json new file mode 100644 index 00000000..661512c2 --- /dev/null +++ b/examples/observed-effect/expected.json @@ -0,0 +1,68 @@ +{ + "trace_signer_jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "ysSfGiHZFwj77FqkuI2D7bc9K0sNHVbPcoDjroN3aeA" + }, + "resolver": "https://observer.example.org/intervals", + "predicate_type": "https://probityai.github.io/agent-evidence-vectors/predicate/v1/observed-effect", + "observer_keys": { + "f3cb9b6750737ef6789a72b71d8305f2": { + "kty": "OKP", + "crv": "Ed25519", + "x": "TypZ7cg2fetABHzoPuf1znEaV9k6u9qdHOhYjFajzog" + } + }, + "cases": { + "01-observation-verified.json": { + "store": "effect-store.json", + "trace_record_verifies": true, + "reference_resolves": true, + "digest_matches": true, + "observer_key_configured": true, + "envelope_verifies": true, + "dual_values": "agree", + "verdict": "observation-verified" + }, + "02-observation-altered-after-issue.json": { + "store": "effect-store-altered.json", + "trace_record_verifies": true, + "reference_resolves": true, + "digest_matches": false, + "observer_key_configured": true, + "envelope_verifies": false, + "dual_values": "agree", + "verdict": "observation-digest-mismatch" + }, + "03-observer-and-observed-disagree.json": { + "store": "effect-store.json", + "trace_record_verifies": true, + "reference_resolves": true, + "digest_matches": true, + "observer_key_configured": true, + "envelope_verifies": true, + "dual_values": "disagree", + "verdict": "observation-verified" + }, + "04-reference-unresolvable.json": { + "store": "effect-store.json", + "trace_record_verifies": true, + "reference_resolves": false, + "digest_matches": null, + "observer_key_configured": null, + "envelope_verifies": null, + "dual_values": null, + "verdict": "observation-unresolved" + }, + "05-observer-key-not-configured.json": { + "store": "effect-store.json", + "trace_record_verifies": true, + "reference_resolves": true, + "digest_matches": true, + "observer_key_configured": false, + "envelope_verifies": null, + "dual_values": "agree", + "verdict": "observation-unverified" + } + } +} diff --git a/examples/observed-effect/gen_observed_effect_vectors.py b/examples/observed-effect/gen_observed_effect_vectors.py new file mode 100644 index 00000000..8c74a2cb --- /dev/null +++ b/examples/observed-effect/gen_observed_effect_vectors.py @@ -0,0 +1,216 @@ +"""Generate examples/observed-effect: five signed Trust Records, each citing an +`observed-effect` reference, against an effect store and an altered copy of it. + +The two statements in `source/` are copied byte for byte from the published +observed-effect conformance corpus, so the referenced objects here are ones a second +implementation already resolves and verifies. Everything this script adds (the Trust +Record producer key, the second observer key, the store and the altered store) derives +from one published seed, so the set regenerates byte for byte and +tests/test_observed_effect_fixtures.py holds the committed files to this script. +Nothing here is a production record. + +Usage: python examples/observed-effect/gen_observed_effect_vectors.py [--out DIR] +""" + +from __future__ import annotations + +import argparse +import base64 +import copy +import hashlib +import json +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +import rfc8785 +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from agentrust_trace import key_to_jwk, sign_record + +SEED = b"trace-spec examples/observed-effect 2026-09-23" +HERE = Path(__file__).resolve().parent + +PREDICATE_TYPE = "https://probityai.github.io/agent-evidence-vectors/predicate/v1/observed-effect" +PAYLOAD_TYPE = "application/vnd.in-toto+json" +RESOLVER = "https://observer.example.org/intervals" +IAT = int(datetime(2026, 9, 19, 0, 0, 10, tzinfo=UTC).timestamp()) + +# The corpus observer key, as its manifest publishes it. The corpus derives it from a +# published seed too, so anyone can rebuild the two source statements. +CORPUS_OBSERVER_PUBLIC = bytes.fromhex( + "4f2a59edc8367deb40047ce83ee7f5ce711a57d93abbda9d1ce8588c56a3ce88" +) +# An authoritative interval in which the observer and the observed party agree. +AGREE = "v1c6fdd82db5229e4" +# The same shape of interval in which they disagree on two facts, which the predicate +# records rather than rejects. +DISAGREE = "v620e7755ba36aa0a" + + +def b64u(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + + +def jcs_sha256(value: Any) -> str: + return "sha256:" + hashlib.sha256(rfc8785.dumps(value)).hexdigest() + + +def key(label: str) -> Ed25519PrivateKey: + return Ed25519PrivateKey.from_private_bytes( + hashlib.sha256(SEED + b"|" + label.encode()).digest() + ) + + +def raw_public(k: Ed25519PrivateKey) -> bytes: + return k.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + + +def keyid(raw: bytes) -> str: + """The corpus rule: the first 32 hex characters of SHA-256 over the raw public key.""" + return hashlib.sha256(raw).hexdigest()[:32] + + +def okp_jwk(raw: bytes) -> dict[str, str]: + return {"kty": "OKP", "crv": "Ed25519", "x": b64u(raw)} + + +def pae(payload_type: str, payload: bytes) -> bytes: + return b"DSSEv1 %d %s %d %s" % (len(payload_type), payload_type.encode(), len(payload), payload) + + +def source(member: str) -> dict[str, Any]: + raw = (HERE / "source" / f"{member}.json").read_bytes() + # A corpus member's identifier is the first 16 hex characters of SHA-256 over its + # file, so a copy that drifted from the corpus fails here rather than downstream. + assert "v" + hashlib.sha256(raw).hexdigest()[:16] == member, member + return json.loads(raw) + + +def resign(envelope: dict[str, Any], signer: Ed25519PrivateKey) -> dict[str, Any]: + """The same payload bytes under a different observer's signature.""" + payload = base64.b64decode(envelope["payload"]) + sig = signer.sign(pae(envelope["payloadType"], payload)) + return { + "payload": envelope["payload"], + "payloadType": envelope["payloadType"], + "signatures": [{"keyid": keyid(raw_public(signer)), "sig": base64.b64encode(sig).decode()}], + } + + +def erase_disagreement(envelope: dict[str, Any]) -> dict[str, Any]: + """The stored copy as a relying party later finds it: every disagreeing row + rewritten to agree with the observed party's report, signature left as issued.""" + altered = copy.deepcopy(envelope) + statement = json.loads(base64.b64decode(altered["payload"])) + for row in statement["predicate"]["dualValues"]: + row["observedValue"] = row["reportedValue"] + row["agreement"] = "agree" + altered["payload"] = base64.b64encode(rfc8785.dumps(statement)).decode() + return altered + + +def record(producer: Ed25519PrivateKey, reference: dict[str, Any]) -> dict[str, Any]: + unsigned = { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": IAT, + "subject": "spiffe://trust.example.org/agent/build-bot", + "model": {"provider": "example", "model_id": "example-model"}, + "runtime": {"platform": "software-only", "measurement": "sha256:" + "0" * 64}, + "policy": {"bundle_hash": "sha256:" + "b" * 64, "enforcement_mode": "enforce"}, + "data_class": "internal", + "build_provenance": {"slsa_level": 1, "digest": "sha256:" + "e" * 64}, + "appraisal": {"status": "none", "verifier": "https://verifier.example.org"}, + "cnf": {"jwk": key_to_jwk(producer)}, + "references": [reference], + } + return sign_record(unsigned, producer) + + +def reference(id_: str, digest: str) -> dict[str, Any]: + return { + "rel": "observed-effect", "id": id_, "resolver": RESOLVER, + "digest": digest, "retention": "P1Y", + } + + +def case(store: str, resolves: bool, matches: bool | None, held: bool | None, + verifies: bool | None, agreement: str | None, verdict: str) -> dict[str, Any]: + return { + "store": store, "trace_record_verifies": True, "reference_resolves": resolves, + "digest_matches": matches, "observer_key_configured": held, + "envelope_verifies": verifies, "dual_values": agreement, "verdict": verdict, + } + + +def build() -> dict[str, Any]: + producer, other_observer = key("producer"), key("other-observer") + agree, disagree = source(AGREE), source(DISAGREE) + + store = { + "resolver": RESOLVER, + "effects": { + "interval/1": agree, + "interval/2": disagree, + "interval/3": resign(agree, other_observer), + }, + } + altered = copy.deepcopy(store) + altered["effects"]["interval/2"] = erase_disagreement(disagree) + + cite = lambda id_: reference(id_, jcs_sha256(store["effects"][id_])) # noqa: E731 + unresolvable = "sha256:" + hashlib.sha256(SEED + b"|digest|interval/9").hexdigest() + records = { + "01-observation-verified.json": record(producer, cite("interval/1")), + "02-observation-altered-after-issue.json": record(producer, cite("interval/2")), + "03-observer-and-observed-disagree.json": record(producer, cite("interval/2")), + "04-reference-unresolvable.json": record(producer, reference("interval/9", unresolvable)), + "05-observer-key-not-configured.json": record(producer, cite("interval/3")), + } + + expected = { + "trace_signer_jwk": key_to_jwk(producer), + "resolver": RESOLVER, + "predicate_type": PREDICATE_TYPE, + # The keys this relying party holds, by DSSE keyid. The second observer's key is + # deliberately absent: spec section 3.3.2 says a receipt whose issuer key is + # unknown to the verifier is unverified, not invalid. + "observer_keys": {keyid(CORPUS_OBSERVER_PUBLIC): okp_jwk(CORPUS_OBSERVER_PUBLIC)}, + "cases": { + "01-observation-verified.json": case( + "effect-store.json", True, True, True, True, "agree", "observation-verified"), + "02-observation-altered-after-issue.json": case( + "effect-store-altered.json", True, False, True, False, "agree", + "observation-digest-mismatch"), + "03-observer-and-observed-disagree.json": case( + "effect-store.json", True, True, True, True, "disagree", "observation-verified"), + "04-reference-unresolvable.json": case( + "effect-store.json", False, None, None, None, None, "observation-unresolved"), + "05-observer-key-not-configured.json": case( + "effect-store.json", True, True, False, None, "agree", "observation-unverified"), + }, + } + + return { + **records, + "effect-store.json": store, + "effect-store-altered.json": altered, + "expected.json": expected, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=HERE) + out = parser.parse_args().out + out.mkdir(parents=True, exist_ok=True) + files = build() + for name, value in files.items(): + text = json.dumps(value, indent=2, ensure_ascii=False) + "\n" + (out / name).write_bytes(text.encode("utf-8")) + print(f"{len(files)} files written to {out}") + + +if __name__ == "__main__": + main() diff --git a/examples/observed-effect/source/v1c6fdd82db5229e4.json b/examples/observed-effect/source/v1c6fdd82db5229e4.json new file mode 100644 index 00000000..daaac2a1 --- /dev/null +++ b/examples/observed-effect/source/v1c6fdd82db5229e4.json @@ -0,0 +1,10 @@ +{ + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMiJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwMSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiM2NiMDJjODk3ZWViN2VhY2JiNTJmYzdmOTJkODcwZTk2NzgxYTRjN2VjNmMzYTljMDNhMGNmN2VlZDQ2YzQzMyIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiMmQxNmFmOGU2ZWZjY2RiYjFkOWFjZjA3MTAzNWVhNWUyNDJhY2JiMTY2NjNhM2QwNTI2MWEwNmNmOTkyZjljZGFhY2IyM2UzM2I5MDQ0ZjFiNGUyZWMyNDQ0ZGY4ZjQzNTI0MDI1MWUwNmNiMmE3MTI2ZWRhNGU4ZjJkZGI3MDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDEifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "f3cb9b6750737ef6789a72b71d8305f2", + "sig": "rW8A8so/7gwe3AcKwNkq1qSxKLjNETKKhOdJIyUSvo0pJrd8VGhtiI3AT6pWV+3yz4FL7Z6zvbqbHQp2gC6OAQ==" + } + ] +} diff --git a/examples/observed-effect/source/v620e7755ba36aa0a.json b/examples/observed-effect/source/v620e7755ba36aa0a.json new file mode 100644 index 00000000..cbcfae80 --- /dev/null +++ b/examples/observed-effect/source/v620e7755ba36aa0a.json @@ -0,0 +1,10 @@ +{ + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGUiOnsiYXV0aG9yaXR5RGlnZXN0IjoiNmQ4ODBjYTc4MmUyZTRkYjU1ZjFlODBjYzU1NGQyOTBiNGY0NjIyMGI3NTI0NWI3ZDFkZThhZmJiMDE3MTlhYiIsImRvZXNOb3RBc3NlcnQiOlsidGhhdCB0aGUgb2JzZXJ2ZWQgcGFydHkgcGVyZm9ybWVkIG5vIGFjdGlvbiBvdXRzaWRlIHBhdGhTY29wZSIsInRoYXQgdGhlIGF1dGhvcml0eSBkb2N1bWVudCBwZXJtaXRzIHdoYXQgdGhlIHdyaXRlcyBkaWQiXSwiZHVhbFZhbHVlcyI6W3siYWdyZWVtZW50IjoiZGlzYWdyZWUiLCJmYWN0Ijoid3JpdGVzLmNvdW50Iiwib2JzZXJ2ZWRWYWx1ZSI6IjIiLCJyZXBvcnRlZFZhbHVlIjoiMSJ9LHsiYWdyZWVtZW50IjoiZGlzYWdyZWUiLCJmYWN0IjoiaW50ZXJ2YWwuYWZ0ZXJSb290Iiwib2JzZXJ2ZWRWYWx1ZSI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJyZXBvcnRlZFZhbHVlIjoiYzQwOTE5Y2RiOGU2NmM5MzVkMzkwNzM1MjQzNmI5ZTZmZTc5NjI3ODk1NWFkNjZjYmZjMWQ5NWY3YWY5OTZmMSJ9XSwiaGFzaEFsZ29yaXRobSI6InNoYTI1NiIsImludGVydmFsIjp7ImFmdGVyUm9vdCI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEiLCJiYXNlUmVzb2x1dGlvbiI6InN1cHBsaWVkIiwiYmVmb3JlUm9vdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEiLCJvcGVuZWRBdCI6IjIwMjYtMDktMTlUMDA6MDA6MDBaIiwic2VhbGVkQXQiOiIyMDI2LTA5LTE5VDAwOjAwOjA0WiJ9LCJpbnRlcnZhbElkIjoiaXYtMDAwNSIsImlzc3VlZEF0IjoiMjAyNi0wOS0xOVQwMDowMDowNVoiLCJtdXRhdGlvbiI6Im9ic2VydmVkIiwib2JzZXJ2YXRpb24iOnsiY292ZXJhZ2UiOnsiZ2FwcyI6W10sInNjb3BlQ29tcGxldGUiOnRydWV9LCJvYnNlcnZlZFNpZ25lcnMiOlsiOTNjNGU1YjY0MGU4M2JhMGVkODdlZmZlOWE4NmYwNWUiXSwib3JpZ2luIjoiZmlyc3QtaGFuZCIsInByaW9yQ29tbWl0bWVudCI6eyJjb21taXRtZW50RGlnZXN0IjoiYmM5OTc5OGFmYmYxYTRhY2Y2MGUyOGM4OTEzYmQ0NjJlNTU1ODk5YzAyZGJkMTBiZGVkYWQzMzViZjRjYzBjYiIsImNvbW1pdHRlZEF0IjoiMjAyNi0wOS0xOFQyMzo1OTo1OFoiLCJrZXlpZCI6ImYzY2I5YjY3NTA3MzdlZjY3ODlhNzJiNzFkODMwNWYyIiwic2lnIjoiNzI4ZmJhN2ViZjJkY2RlMzVhNTk2YWEzOGQ3NTZiNjIwM2Y2ZDIxM2JkYThmMzM3NzUyYzRkZTBmYjdhNTZkM2I2ZDQ2ZDlkOTJhMTVlMzY3YzNhMDk1MTczNGVhZjc4MDYzYTNiN2U5OTc5OGE2ODIyMjI3NGM0NWZlMTdkMDQiLCJ3aXRuZXNzTm9uY2UiOiI2YWEyNGVhYjg5NzM0OGMzNmZiNjMyMTI3MDk1OWFkYmVjOWNjNGViNTUwZDYyMjVkNDU4MzNlYzBkNjNiN2U5In0sInZhbnRhZ2UiOiJiZWxvdy1vYnNlcnZlZCJ9LCJwYXRoU2NvcGUiOlsiL3Nydi9hcHAvIl0sInJlYWRzIjpbeyJibG9iRGlnZXN0IjoiODNjOWY0NjRmOTFkZjQ1NGEwMDJmODkwMTcwY2FjOWYzMTUyYmMzNjgxM2M0NzdiMGVlOGFiYmU3OWEzNjlhYyIsImJ5dGVSYW5nZSI6eyJlbmQiOjY0LCJzdGFydCI6MH0sInBhdGgiOiIvc3J2L2FwcC9jb25maWcueWFtbCIsInByZVN0YXRlRGlnZXN0IjoiZmU4MTYzMzhhZDllZmE3Mjc0YjUwMDk5NGYwMjIxNDk1OGFmNzg4NjZjNTZmYzQwNGYxZTcyMGYyYzg3ZThjMSIsInJhbmdlRGlnZXN0IjoiY2IwZWM2MDQxNTZiNjFlMjZjM2Q4OThhYmUxOTEzZGMwOGZlNTNkNTYzMTU5NTgzZWQ0NWM1YzJhMzU5MjM0NiIsInJlYWRTdGF0ZSI6ImJ5dGVzLXJlYWQifV0sInRpZXIiOiJhdXRob3JpdGF0aXZlIiwid3JpdGVzIjpbeyJpblNjb3BlIjp0cnVlLCJwYXRoIjoiL3Nydi9hcHAvbWFpbi5weSIsInBvc3RTdGF0ZURpZ2VzdCI6IjcwNzRlM2EzNzEyZmE2OTQ5YjQzNThjYTkzNjU1N2U4MWU1ZGUxNDE5MjhkMTcyNWIwZTQ2MDJiMWExYWNiZDMiLCJwcmVTdGF0ZURpZ2VzdCI6ImZlODE2MzM4YWQ5ZWZhNzI3NGI1MDA5OTRmMDIyMTQ5NThhZjc4ODY2YzU2ZmM0MDRmMWU3MjBmMmM4N2U4YzEifSx7ImluU2NvcGUiOnRydWUsInBhdGgiOiIvc3J2L2FwcC9oYW5kbGVyLnB5IiwicG9zdFN0YXRlRGlnZXN0IjoiZTcyYTQ0MWRkNTIxYjVjZDFkNjYxYmUxZjY1NjllMGY0M2I2MTdjMzQ3NjYwMTRjNTgwMGVjZWM0MGNjMDFkMSIsInByZVN0YXRlRGlnZXN0IjoiNzA3NGUzYTM3MTJmYTY5NDliNDM1OGNhOTM2NTU3ZTgxZTVkZTE0MTkyOGQxNzI1YjBlNDYwMmIxYTFhY2JkMyJ9XX0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Byb2JpdHlhaS5naXRodWIuaW8vYWdlbnQtZXZpZGVuY2UtdmVjdG9ycy9wcmVkaWNhdGUvdjEvb2JzZXJ2ZWQtZWZmZWN0Iiwic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImU3MmE0NDFkZDUyMWI1Y2QxZDY2MWJlMWY2NTY5ZTBmNDNiNjE3YzM0NzY2MDE0YzU4MDBlY2VjNDBjYzAxZDEifSwibmFtZSI6Iml2LTAwMDUifV19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "f3cb9b6750737ef6789a72b71d8305f2", + "sig": "4MbIcwmjIjbpak6tDi+F1mlfLu8AV28k0Zg6+x/C8tXax2emMFWuEX8uQ0sXISgiW2UQn9DyxecijLsgRcXrDA==" + } + ] +} diff --git a/schema/trace-claim-v0.3-draft.json b/schema/trace-claim-v0.3-draft.json index 05277ee6..ddd6c305 100644 --- a/schema/trace-claim-v0.3-draft.json +++ b/schema/trace-claim-v0.3-draft.json @@ -357,7 +357,7 @@ "rel": { "type": "string", "minLength": 1, - "description": "Relationship type. The registered values are a registry that grows, so this is not a closed set. authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence. condition-appraisal: an independent check's finding on whether a stated condition is established by a stated subject, bound to both by digest." + "description": "Relationship type. The registered values are a registry that grows, so this is not a closed set. authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence. condition-appraisal: an independent check's finding on whether a stated condition is established by a stated subject, bound to both by digest. observed-effect: a signed record of the state change an observer outside the agent saw over one interval, with the state before and after, the paths observed and the authority change ran under." }, "id": { "type": "string", diff --git a/schema/trace-claim.json b/schema/trace-claim.json index dafefa27..0b65048e 100644 --- a/schema/trace-claim.json +++ b/schema/trace-claim.json @@ -286,7 +286,7 @@ "rel": { "type": "string", "minLength": 1, - "description": "Relationship type. The registered values are a registry that grows, so this is not a closed set. authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence. condition-appraisal: an independent check's finding on whether a stated condition is established by a stated subject, bound to both by digest." + "description": "Relationship type. The registered values are a registry that grows, so this is not a closed set. authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence. condition-appraisal: an independent check's finding on whether a stated condition is established by a stated subject, bound to both by digest. observed-effect: a signed record of the state change an observer outside the agent saw over one interval, with the state before and after, the paths observed and the authority change ran under." }, "id": { "type": "string", diff --git a/spec/trace-v0.2.md b/spec/trace-v0.2.md index 1001a84c..ad4511ca 100644 --- a/spec/trace-v0.2.md +++ b/spec/trace-v0.2.md @@ -226,6 +226,7 @@ Registered `rel` values: - **`approval-outcome`**. An attributable human approval attached to a step-up or defer decision. - **`behavior-trace`**. A behavioural record of what the agent did, of which this record is the environment evidence. - **`condition-appraisal`**. An independent check's finding on whether a stated condition is established by a stated subject: a test run, a schema validation, a contract check. The referenced object binds the condition and the subject by digest and carries the outcome in the checker's own vocabulary. +- **`observed-effect`**. A signed record of the state change an observer outside the agent saw over one interval: the state before and after, the paths observed, and the authority change ran under. 1. `references` MUST NOT affect `runtime.platform`. A record carrying `references` and no `origin` block is `self` and carries whatever platform value it actually earned. 2. The record signature MUST cover `references`, under the canonicalisation in §3.2.2. diff --git a/src/agentrust_trace/schema/trace-v0.2.json b/src/agentrust_trace/schema/trace-v0.2.json index dafefa27..0b65048e 100644 --- a/src/agentrust_trace/schema/trace-v0.2.json +++ b/src/agentrust_trace/schema/trace-v0.2.json @@ -286,7 +286,7 @@ "rel": { "type": "string", "minLength": 1, - "description": "Relationship type. The registered values are a registry that grows, so this is not a closed set. authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence. condition-appraisal: an independent check's finding on whether a stated condition is established by a stated subject, bound to both by digest." + "description": "Relationship type. The registered values are a registry that grows, so this is not a closed set. authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence. condition-appraisal: an independent check's finding on whether a stated condition is established by a stated subject, bound to both by digest. observed-effect: a signed record of the state change an observer outside the agent saw over one interval, with the state before and after, the paths observed and the authority change ran under." }, "id": { "type": "string", diff --git a/tests/test_adequacy_all_sets.py b/tests/test_adequacy_all_sets.py index 03e5f4e2..a8c11b47 100644 --- a/tests/test_adequacy_all_sets.py +++ b/tests/test_adequacy_all_sets.py @@ -288,6 +288,11 @@ def test_the_loader_reads_a_different_set_for_each_name() -> None: "condition-appraisal": "tests/test_condition_appraisal_fixtures.py, which recomputes " "every verdict from the committed record and appraisal store " "instead of reading expected.json, and re-runs the generator", + # Not loadable here for the same reason: every record verifies, and the cases + # differ only in what the reference resolves to in the effect store beside them. + "observed-effect": "tests/test_observed_effect_fixtures.py, which recomputes every " + "verdict from the committed record and effect store instead of " + "reading expected.json, and re-runs the generator", } diff --git a/tests/test_condition_appraisal_fixtures.py b/tests/test_condition_appraisal_fixtures.py index d1e27b53..ef3b265f 100644 --- a/tests/test_condition_appraisal_fixtures.py +++ b/tests/test_condition_appraisal_fixtures.py @@ -69,8 +69,11 @@ def _assess(record: dict, store: dict, issuer_keys: dict) -> dict: Three separable findings: whether the reference resolves, whether the resolved bytes are the cited bytes, and whether the object verifies under its named issuer's key - when the relying party holds that key. The outcome is reported as the object states - it and decides nothing here: section 3.1.2 rule 3 makes identity the ceiling. + when the relying party holds that key. Each is reported in its own field. The verdict + names the state of the referenced appraisal, never the condition it reports on: a + verified appraisal does not establish that the condition held, and a digest mismatch + does not establish that it did not. The outcome is reported as the object states it + and decides nothing here: section 3.1.2 rule 3 makes identity the ceiling. """ (reference,) = record["references"] assert reference["rel"] == "condition-appraisal" @@ -78,19 +81,19 @@ def _assess(record: dict, store: dict, issuer_keys: dict) -> dict: obj = store["appraisals"].get(reference["id"]) if obj is None: return {"reference_resolves": False, "digest_matches": None, "issuer_key_configured": None, - "appraisal_verifies": None, "outcome": None, "verdict": "appraisal-unconfirmed"} + "appraisal_verifies": None, "outcome": None, "verdict": "appraisal-unresolved"} digest_matches = _jcs_sha256(obj) == reference["digest"] jwk = issuer_keys.get(obj["issuer_key_id"]) configured = jwk is not None verifies = _appraisal_verifies(obj, jwk) if configured else None if not digest_matches: - verdict = "appraisal-contradicted" + verdict = "appraisal-digest-mismatch" elif not configured: verdict = "appraisal-unverified" elif not verifies: - verdict = "appraisal-contradicted" + verdict = "appraisal-signature-invalid" else: - verdict = "appraisal-confirmed" + verdict = "appraisal-verified" return {"reference_resolves": True, "digest_matches": digest_matches, "issuer_key_configured": configured, "appraisal_verifies": verifies, "outcome": obj["outcome"]["status"], "verdict": verdict} @@ -106,7 +109,7 @@ def test_the_committed_records_are_exactly_the_declared_cases() -> None: @pytest.mark.parametrize("name", CASES) def test_every_case_verifies_as_a_trust_record(name: str) -> None: # Section 3.1.2 rule 3: what a reference resolves to never decides whether the - # record verifies, so the contradicted, unverified and unresolvable cases verify too. + # record verifies, so the mismatched, unverified and unresolved cases verify too. verify_record(_load(name), EXPECTED["trace_signer_jwk"], max_age_seconds=None) assert EXPECTED["cases"][name]["trace_record_verifies"] is True @@ -123,14 +126,14 @@ def test_a_pass_and_a_fail_verify_identically() -> None: records differ only in the reference they carry, both verify, and the relying party's assessment differs only in the reported outcome.""" store = _load("appraisal-store.json") - passing = _assess(_load("01-appraisal-confirmed.json"), store, EXPECTED["issuer_keys"]) + passing = _assess(_load("01-appraisal-verified.json"), store, EXPECTED["issuer_keys"]) failing = _assess(_load("03-outcome-is-a-fail.json"), store, EXPECTED["issuer_keys"]) assert (passing["outcome"], failing["outcome"]) == ("pass", "fail") assert {k: v for k, v in passing.items() if k != "outcome"} == \ {k: v for k, v in failing.items() if k != "outcome"} - for name in ("01-appraisal-confirmed.json", "03-outcome-is-a-fail.json"): + for name in ("01-appraisal-verified.json", "03-outcome-is-a-fail.json"): verify_record(_load(name), EXPECTED["trace_signer_jwk"], max_age_seconds=None) - passing_record = _load("01-appraisal-confirmed.json") + passing_record = _load("01-appraisal-verified.json") failing_record = _load("03-outcome-is-a-fail.json") strip = lambda r: { # noqa: E731 k: v for k, v in r.items() if k not in ("references", "signature") @@ -138,8 +141,26 @@ def test_a_pass_and_a_fail_verify_identically() -> None: assert strip(passing_record) == strip(failing_record) +def test_a_bad_signature_over_matching_bytes_is_its_own_verdict() -> None: + """A corrupted issuer signature, with the reference digest recomputed over the + corrupted object, matches on digest and fails on signature. The verdict says so and + does not fold it into a digest mismatch.""" + store = copy.deepcopy(_load("appraisal-store.json")) + obj = store["appraisals"]["appraisal/1"] + signature = _b64u_decode(obj["signature"]) + obj["signature"] = base64.urlsafe_b64encode( + bytes([signature[0] ^ 1]) + signature[1:] + ).rstrip(b"=").decode() + record = copy.deepcopy(_load("01-appraisal-verified.json")) + record["references"][0]["digest"] = _jcs_sha256(obj) + observed = _assess(record, store, EXPECTED["issuer_keys"]) + assert observed["digest_matches"] is True + assert observed["appraisal_verifies"] is False + assert observed["verdict"] == "appraisal-signature-invalid" + + def test_the_record_signature_covers_the_reference() -> None: - record = copy.deepcopy(_load("01-appraisal-confirmed.json")) + record = copy.deepcopy(_load("01-appraisal-verified.json")) digest = record["references"][0]["digest"] record["references"][0]["digest"] = digest[:-1] + ("0" if digest[-1] != "0" else "1") with pytest.raises(InvalidSignature): diff --git a/tests/test_observed_effect_fixtures.py b/tests/test_observed_effect_fixtures.py new file mode 100644 index 00000000..17e92106 --- /dev/null +++ b/tests/test_observed_effect_fixtures.py @@ -0,0 +1,231 @@ +"""An observed mutation interval as a TRACE `observed-effect` reference. + +Re-verifies examples/observed-effect/ from the committed bytes. The generator wrote +expected.json alongside the vectors, and a generator's own summary of its output is a +claim, so every outcome here is recomputed with this repository's dependencies and +compared with expected.json, never read from it. The generator is deterministic, and +the last test re-runs it against the committed files. +""" + +from __future__ import annotations + +import base64 +import copy +import hashlib +import json +import subprocess +import sys +from pathlib import Path + +import pytest +import rfc8785 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from agentrust_trace import verify_record + +DIR = Path(__file__).resolve().parents[1] / "examples" / "observed-effect" +GENERATOR = DIR / "gen_observed_effect_vectors.py" +EXPECTED = json.loads((DIR / "expected.json").read_text(encoding="utf-8")) +CASES = sorted(EXPECTED["cases"]) +OUTCOME_KEYS = ( + "reference_resolves", "digest_matches", "observer_key_configured", + "envelope_verifies", "dual_values", "verdict", +) + + +def _load(name: str) -> dict: + return json.loads((DIR / name).read_text(encoding="utf-8")) + + +def _jcs_sha256(obj: object) -> str: + return "sha256:" + hashlib.sha256(rfc8785.dumps(obj)).hexdigest() + + +def _b64u_decode(text: str) -> bytes: + return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) + + +def _pae(payload_type: str, payload: bytes) -> bytes: + return b"DSSEv1 %d %s %d %s" % (len(payload_type), payload_type.encode(), len(payload), payload) + + +def _statement(envelope: dict) -> dict: + return json.loads(base64.b64decode(envelope["payload"])) + + +def _envelope_verifies(envelope: dict, jwk: dict) -> bool: + """The object's own check: Ed25519 over the DSSE pre-authentication encoding.""" + (signature,) = envelope["signatures"] + message = _pae(envelope["payloadType"], base64.b64decode(envelope["payload"])) + try: + Ed25519PublicKey.from_public_bytes(_b64u_decode(jwk["x"])).verify( + base64.b64decode(signature["sig"]), message + ) + except InvalidSignature: + return False + return True + + +def _dual_values(envelope: dict) -> str: + rows = _statement(envelope)["predicate"].get("dualValues", []) + return "disagree" if any(r["agreement"] == "disagree" for r in rows) else "agree" + + +def _assess(record: dict, store: dict, observer_keys: dict) -> dict: + """What a relying party concludes about the object a record points at. + + Three separable findings: whether the reference resolves, whether the resolved bytes + are the cited bytes, and whether the envelope verifies under its observer's key when + the relying party holds that key. Each is reported in its own field. The verdict + names the state of the referenced observation, never the effect it reports: a + verified observation does not establish that the change occurred, and a digest + mismatch does not establish that it did not. What the statement reports is carried + as it states it and decides nothing here: section 3.1.2 rule 3 makes identity the + ceiling. + """ + (reference,) = record["references"] + assert reference["rel"] == "observed-effect" + assert reference["resolver"] == store["resolver"] == EXPECTED["resolver"] + envelope = store["effects"].get(reference["id"]) + if envelope is None: + return {"reference_resolves": False, "digest_matches": None, + "observer_key_configured": None, "envelope_verifies": None, + "dual_values": None, "verdict": "observation-unresolved"} + digest_matches = _jcs_sha256(envelope) == reference["digest"] + (signature,) = envelope["signatures"] + jwk = observer_keys.get(signature["keyid"]) + configured = jwk is not None + verifies = _envelope_verifies(envelope, jwk) if configured else None + if not digest_matches: + verdict = "observation-digest-mismatch" + elif not configured: + verdict = "observation-unverified" + elif not verifies: + verdict = "observation-signature-invalid" + else: + verdict = "observation-verified" + return {"reference_resolves": True, "digest_matches": digest_matches, + "observer_key_configured": configured, "envelope_verifies": verifies, + "dual_values": _dual_values(envelope), "verdict": verdict} + + +def test_the_committed_records_are_exactly_the_declared_cases() -> None: + committed = {p.name for p in DIR.glob("0*.json")} + assert committed == set(CASES) + for case in EXPECTED["cases"].values(): + assert (DIR / case["store"]).is_file() + + +@pytest.mark.parametrize("name", CASES) +def test_every_case_verifies_as_a_trust_record(name: str) -> None: + # Section 3.1.2 rule 3: what a reference resolves to never decides whether the + # record verifies, so the mismatched, unverified and unresolved cases verify too. + verify_record(_load(name), EXPECTED["trace_signer_jwk"], max_age_seconds=None) + assert EXPECTED["cases"][name]["trace_record_verifies"] is True + + +@pytest.mark.parametrize("name", CASES) +def test_the_outcome_is_recomputed_from_the_committed_bytes(name: str) -> None: + case = EXPECTED["cases"][name] + observed = _assess(_load(name), _load(case["store"]), EXPECTED["observer_keys"]) + assert observed == {k: case[k] for k in OUTCOME_KEYS} + + +def test_every_resolved_object_carries_the_registered_predicate_type() -> None: + for store in ("effect-store.json", "effect-store-altered.json"): + for envelope in _load(store)["effects"].values(): + assert envelope["payloadType"] == "application/vnd.in-toto+json" + assert _statement(envelope)["predicateType"] == EXPECTED["predicate_type"] + + +def test_the_subject_digest_is_the_interval_after_root() -> None: + """The statement binds its single subject to the state the interval ended in, so + the reference identifies a state change, not only a document about one.""" + for envelope in _load("effect-store.json")["effects"].values(): + statement = _statement(envelope) + (subject,) = statement["subject"] + assert subject["digest"]["sha256"] == statement["predicate"]["interval"]["afterRoot"] + + +def test_an_agreement_and_a_disagreement_verify_identically() -> None: + """What the statement reports never reaches validity. `01` cites an interval where + the observer and the observed party agree and `03` one where they disagree; the two + records differ only in the reference they carry, both verify, and the relying + party's assessment differs only in what it reports.""" + store = _load("effect-store.json") + agree = _assess(_load("01-observation-verified.json"), store, EXPECTED["observer_keys"]) + disagree = _assess( + _load("03-observer-and-observed-disagree.json"), store, EXPECTED["observer_keys"] + ) + assert (agree["dual_values"], disagree["dual_values"]) == ("agree", "disagree") + assert {k: v for k, v in agree.items() if k != "dual_values"} == \ + {k: v for k, v in disagree.items() if k != "dual_values"} + strip = lambda r: { # noqa: E731 + k: v for k, v in r.items() if k not in ("references", "signature") + } + assert strip(_load("01-observation-verified.json")) == \ + strip(_load("03-observer-and-observed-disagree.json")) + + +def test_a_bad_signature_over_matching_bytes_is_its_own_verdict() -> None: + """A corrupted observer signature, with the reference digest recomputed over the + corrupted envelope, matches on digest and fails on signature. The verdict says so and + does not fold it into a digest mismatch.""" + store = copy.deepcopy(_load("effect-store.json")) + envelope = store["effects"]["interval/1"] + (signature,) = envelope["signatures"] + raw = base64.b64decode(signature["sig"]) + signature["sig"] = base64.b64encode(bytes([raw[0] ^ 1]) + raw[1:]).decode() + record = copy.deepcopy(_load("01-observation-verified.json")) + record["references"][0]["digest"] = _jcs_sha256(envelope) + observed = _assess(record, store, EXPECTED["observer_keys"]) + assert observed["digest_matches"] is True + assert observed["envelope_verifies"] is False + assert observed["verdict"] == "observation-signature-invalid" + + +def test_the_record_signature_covers_the_reference() -> None: + record = copy.deepcopy(_load("01-observation-verified.json")) + digest = record["references"][0]["digest"] + record["references"][0]["digest"] = digest[:-1] + ("0" if digest[-1] != "0" else "1") + with pytest.raises(InvalidSignature): + verify_record(record, EXPECTED["trace_signer_jwk"], max_age_seconds=None) + + +def test_every_observer_key_id_is_derived_from_the_key_it_names() -> None: + for kid, jwk in EXPECTED["observer_keys"].items(): + assert kid == hashlib.sha256(_b64u_decode(jwk["x"])).hexdigest()[:32] + held = set(EXPECTED["observer_keys"]) + named = { + envelope["signatures"][0]["keyid"] + for envelope in _load("effect-store.json")["effects"].values() + } + assert named - held, "no case exercises an observer whose key the relying party does not hold" + + +def test_the_altered_store_differs_in_one_payload_only() -> None: + original, altered = _load("effect-store.json"), _load("effect-store-altered.json") + assert original["resolver"] == altered["resolver"] + assert set(original["effects"]) == set(altered["effects"]) + changed = [id_ for id_ in original["effects"] + if original["effects"][id_] != altered["effects"][id_]] + assert changed == ["interval/2"] + before, after = original["effects"]["interval/2"], altered["effects"]["interval/2"] + assert before["signatures"] == after["signatures"] + assert (_dual_values(before), _dual_values(after)) == ("disagree", "agree") + + +def test_the_source_statements_are_the_corpus_members_they_name() -> None: + for path in (DIR / "source").glob("*.json"): + assert "v" + hashlib.sha256(path.read_bytes()).hexdigest()[:16] == path.stem + + +def test_the_generator_reproduces_the_committed_bytes(tmp_path: Path) -> None: + subprocess.run([sys.executable, str(GENERATOR), "--out", str(tmp_path)], check=True, + capture_output=True) + produced = {p.name for p in tmp_path.iterdir()} + committed = {p.name for p in DIR.iterdir() if p.suffix == ".json"} + assert produced == committed + for name in committed: + assert (tmp_path / name).read_bytes() == (DIR / name).read_bytes(), name diff --git a/tests/test_references_block.py b/tests/test_references_block.py index d8b5852e..304a5eb2 100644 --- a/tests/test_references_block.py +++ b/tests/test_references_block.py @@ -81,6 +81,7 @@ def _with(references) -> dict: ("rel approval-outcome", [_entry(rel="approval-outcome")], True), ("rel behavior-trace", [_entry(rel="behavior-trace")], True), ("rel condition-appraisal", [_entry(rel="condition-appraisal")], True), + ("rel observed-effect", [_entry(rel="observed-effect")], True), ("sha384 digest", [_entry(digest="sha384:" + "b" * 96)], True), # rel is a registry rather than a closed set (spec 3.1.2, unlike 3.1.1 on kind), # so a value this schema has never heard of is accepted rather than rejected. @@ -271,7 +272,10 @@ def test_the_registered_rel_values_stay_documented_in_all_three_places() -> None """ from agentrust_trace import SCHEMA - registered = ("authorized-intent", "approval-outcome", "behavior-trace", "condition-appraisal") + registered = ( + "authorized-intent", "approval-outcome", "behavior-trace", "condition-appraisal", + "observed-effect", + ) rel = SCHEMA["properties"]["references"]["items"]["properties"]["rel"] assert "enum" not in rel, ( "rel is a registry; closing it makes every new relation a schema change too"