From 84bbda46ea72849ac4c5a3a98fabe0802df53dc1 Mon Sep 17 00:00:00 2001 From: yoshi49535 Date: Sat, 1 Aug 2026 14:53:49 +0900 Subject: [PATCH] spec: define exact EvidenceView appraisal contract --- .github/workflows/lint.yml | 2 + GLOSSARY.ja.md | 1 + GLOSSARY.md | 1 + README.ja.md | 2 +- README.md | 2 +- rules/audit.yaml | 2 +- rules/claims.yaml | 8 +- rules/evidence-view.yaml | 2 +- rules/external-effect.yaml | 56 ++++++------ tools/lint.py | 2 +- tools/rule.schema.json | 2 +- tools/verify_evidence_vectors.py | 145 +++++++++++++++++++++++++++++++ vectors/evidence-view-001.json | 42 +++++++++ vectors/evidence-view-002.json | 34 ++++++++ 14 files changed, 263 insertions(+), 38 deletions(-) create mode 100644 tools/verify_evidence_vectors.py create mode 100644 vectors/evidence-view-001.json create mode 100644 vectors/evidence-view-002.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 411c306..297dc13 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,3 +25,5 @@ jobs: run: python tools/lint.py - name: vector-vs-schema validation run: python tools/validate_vectors.py + - name: independent EvidenceView vectors + run: python tools/verify_evidence_vectors.py diff --git a/GLOSSARY.ja.md b/GLOSSARY.ja.md index 2438013..495da5c 100644 --- a/GLOSSARY.ja.md +++ b/GLOSSARY.ja.md @@ -12,4 +12,5 @@ non-normative。用語は表の 1 行で定義し、説明文を書き足さな | チェーン起点 | previousCredential を持たず、新しいチェーンを開始するクレデンシャル | | process type | pipeline process の振る舞いの分類(Chained Process / Source Process / Sink Process / Custom Process)。判別は wire 上の署名挙動による: Source Process は chain を起こし(chain origin)、Chained Process は繋ぎ(previousCredential を運ぶ)、Sink Process は終える(検証する。デフォルトでは in-network に何も発行しない — wire profile は delivery receipt の発行を許可してよい、process.sink.receipt 参照)。全参加者は Process DID を持つ — catalog が分類するのは process。(旧称 component type: FilterConvert / Origin Source / External Sink / Custom — 0.x の間併記、GA で除去) | | audit-reachable | 集約境界をまたぐ監査到達性を提供する optional な conformance class(→ rules/commitment.yaml) | +| effect-gated | exact な受理済み evidence view を外部効果 state machine に結合する optional な conformance class(→ rules/external-effect.yaml) | | trust layering(L1/L2/L3) | L1 = per-credential 自己整合(3 確信度軸、→ rules/confidence.yaml)、L2 = 監査到達性(source commitment による消費ソース集合への遡及、audit-reachable class)、L3 = 意味的監査(申告ソースから出力が正しく導出されたかの判定)。protocol が保証するのは L1+L2 まで — L3 は protocol の決定範囲外(adapter / 監査基盤の関心事) | diff --git a/GLOSSARY.md b/GLOSSARY.md index 15b79eb..5a53068 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -12,6 +12,7 @@ Non-normative. Each term is defined in a single table row; no explanatory prose | chain origin | A credential that carries no previousCredential and starts a new chain | | process type | The classification of a pipeline process's behavior (Chained Process / Source Process / Sink Process / Custom Process), determined by signing behavior on the wire: a Source Process starts a chain (chain origin), a Chained Process continues it (carries previousCredential), a Sink Process ends it (verifies; by default emits nothing in-network — a wire profile can opt in to delivery receipts, see process.sink.receipt). Every pipeline participant holds a Process DID — the catalog classifies processes. (Formerly "component type": FilterConvert / Origin Source / External Sink / Custom — note kept through 0.x, removed at GA) | | audit-reachable | An optional conformance class providing audit reachability across aggregation boundaries (see rules/commitment.yaml) | +| effect-gated | An optional conformance class binding an exact accepted evidence view to an external-effect state machine (see rules/external-effect.yaml) | | trust layering (L1/L2/L3) | Reserved for authentication (AUTH) contexts — the per-request bearer/PDP and wire-authentication surfaces. Provenance evaluation is no longer expressed as L1/L2/L3; it uses named scopes (rules/claims.yaml: claims.scope.named). The former provenance layering gloss (per-credential axes / audit reachability / semantic audit) is superseded by the scope catalog; the three confidence axes (rules/confidence.yaml) remain an internal composition input | | named scope | A versioned identifier for one evaluation the protocol makes about a credential or chain (rules/claims.yaml) — the external claim vocabulary that replaces provenance L1/L2/L3. Examples: CREDENTIAL_ATTESTATION, LINEAR_ATTESTATION@1, SOURCE_SET_BINDING, SIGNATURE_ONLY@1 | | evidence vector | The list of named scopes a verifier reports, each with a coverage and (when evaluated) a truth-state (claims.coverage.three-value). A durable artifact; crossing a trust boundary it is committed by a signature or checkpoint (claims.vector.durable-commit) | diff --git a/README.ja.md b/README.ja.md index 63de27a..51b680a 100644 --- a/README.ja.md +++ b/README.ja.md @@ -36,7 +36,7 @@ markdown(本書、concept.md、GLOSSARY.md 等)はすべて non-normative。 | --- | --- | | `id` | `.[.]`。`status: todo` の間は仮 id、`draft` 昇格で凍結。ただし最初の consumed release までは、台帳記録を伴う意図的な rename + タグ切り直しを許す(VERSIONING.md と STATUS ledger 参照)— consumed release 以降は draft 昇格が hard freeze | | `status` | `todo`(転記前 stub)/ `draft` / `stable` | -| `class` | `core`(既定)/ `audit-reachable`。conformance class の所属 | +| `class` | `core`(既定)/ `audit-reachable` / `effect-gated`。conformance class の所属。後二者は opt-in | | `statement` | 規範文。`draft` 以上で必須、256 文字以内、RFC 2119 の規範語を 1 つ以上含む。1 rule = 1 表現で、他所での再表現は禁止 | | `uses` | 依存する rule id の列。規範の再掲の代わりに参照する | | `schemas` / `vectors` | 対応する artifact ファイルへの参照 | diff --git a/README.md b/README.md index 92b0506..45c7776 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Each entry in `rules/*.yaml`: | --- | --- | | `id` | `.[.]`. Provisional while `status: todo`; frozen on promotion to `draft`. Until the first consumed release, a tagged-and-re-cut rename remains possible as a deliberate, ledgered act (see VERSIONING.md and the STATUS ledger) — from the first consumed release onward, draft promotion is the hard freeze point | | `status` | `todo` (stub before transcription) / `draft` / `stable` | -| `class` | `core` (default) / `audit-reachable`. Conformance class membership | +| `class` | `core` (default) / `audit-reachable` / `effect-gated`. Conformance class membership; the latter two are opt-in | | `statement` | The normative statement. A hard requirement for `draft` and above; at most 256 characters; contains at least one RFC 2119 normative keyword. One rule, one expression — re-expressing elsewhere is prohibited | | `uses` | List of rule ids this rule depends on. Use cross-references instead of repeating normative text | | `schemas` / `vectors` | References to corresponding artifact files | diff --git a/rules/audit.yaml b/rules/audit.yaml index 62a368c..a8236bc 100644 --- a/rules/audit.yaml +++ b/rules/audit.yaml @@ -67,7 +67,7 @@ rules: - id: audit.release.synthesis status: draft - class: core + class: effect-gated statement: >- A ReleaseAuthorization issuer MUST synthesize the decision from the per-scope evidence vector committed for the exact EvidenceViewID and diff --git a/rules/claims.yaml b/rules/claims.yaml index 06de692..816c084 100644 --- a/rules/claims.yaml +++ b/rules/claims.yaml @@ -324,7 +324,7 @@ rules: - id: claims.profile.external-effect status: draft - class: core + class: effect-gated statement: >- The external-effect profile MUST require the provenance-release and current-authorization scopes (KEY_AUTHORIZATION_AT_STATE, @@ -464,7 +464,7 @@ rules: - id: claims.effect.contracts status: draft - class: core + class: effect-gated statement: >- The external-effect contract identifiers MUST be exactly the set enumerated in scope-catalog.json, and a consumer MUST derive effect @@ -480,7 +480,7 @@ rules: - id: claims.effect.legacy-receipt status: draft - class: core + class: effect-gated statement: >- A pre-B2 sink receipt MUST be projected only to LEGACY_SINK_RECEIPT@1; it MUST NOT be re-projected to EFFECT_CONFIRMED@1 or any release or @@ -496,7 +496,7 @@ rules: - id: claims.effect.scope-mapping status: draft - class: core + class: effect-gated statement: >- The RECEIPT_EXTERNAL_EFFECT@1 scope MUST be evaluated only from an EFFECT_CONFIRMED@1 record bound to the delivery identity; a receipt diff --git a/rules/evidence-view.yaml b/rules/evidence-view.yaml index 6386fa4..75ba2aa 100644 --- a/rules/evidence-view.yaml +++ b/rules/evidence-view.yaml @@ -27,7 +27,7 @@ rules: be the digest of that manifest. uses: [evidence.manifest.persisted, identity.variant.immutable-set] schemas: [evidence-view.json] - vectors: [] + vectors: [evidence-view-001, evidence-view-002] notes: >- The EvidenceViewID is the single handle that binds a verdict to the exact spine, contract, and snapshots it was computed under. The id is computed diff --git a/rules/external-effect.yaml b/rules/external-effect.yaml index 7d809a0..71f3bc0 100644 --- a/rules/external-effect.yaml +++ b/rules/external-effect.yaml @@ -8,7 +8,7 @@ rules: - id: effect.sink.classification status: draft - class: core + class: effect-gated statement: >- A sink deployment MUST be classified under a versioned sink profile — external-effect-sink@1 or archive-observation-sink@1 — and an archive @@ -27,7 +27,7 @@ rules: - id: effect.gate.verified-scopes status: draft - class: core + class: effect-gated statement: >- An external-effect-sink@1 deployment MUST execute an irreversible external effect only after every scope required by its claim contract is @@ -43,7 +43,7 @@ rules: - id: effect.gate.full-spine status: draft - class: core + class: effect-gated statement: >- Full chain for an external-effect release MUST mean the complete previousCredential spine from head to origin; every additional scope the @@ -59,7 +59,7 @@ rules: - id: effect.gate.no-bare-verdict status: draft - class: core + class: effect-gated statement: >- A release decision MUST NOT be derived from an overall or aggregate verdict, a body-level latest verdict, or an adjacent-only verdict; the @@ -76,7 +76,7 @@ rules: - id: effect.state.closed-machine status: draft - class: core + class: effect-gated statement: >- Effect state MUST use only the ten states and the transition table fixed by effect-status.json; a deployment MUST NOT add states or @@ -98,7 +98,7 @@ rules: - id: effect.authorization.artifact status: draft - class: core + class: effect-gated statement: >- A release MUST be authorized by an immutable, integrity-protected ReleaseAuthorization committing to the field set fixed by @@ -118,7 +118,7 @@ rules: - id: effect.authorization.wire-artifact status: draft - class: core + class: effect-gated statement: >- The authorization path from evidence evaluation to release MUST cross a versioned wire artifact; a release worker MUST NOT consume @@ -135,7 +135,7 @@ rules: - id: effect.release.exact-bytes status: draft - class: core + class: effect-gated statement: >- A release worker MUST match the ReleaseAuthorization against the exact stored quarantine bytes (payload digest and wire variant) and MUST NOT @@ -150,7 +150,7 @@ rules: - id: effect.release.freshness status: draft - class: core + class: effect-gated statement: >- A ReleaseAuthorization past valid_until, or whose policy, lifecycle freshness, or input snapshots no longer match, MUST NOT dispatch; the @@ -166,7 +166,7 @@ rules: - id: effect.quarantine.atomic-entry status: draft - class: core + class: effect-gated statement: >- A quarantine entry MUST atomically persist the credential wire variant, payload bytes, payload digest, delivery metadata, and sink profile id; a @@ -183,7 +183,7 @@ rules: - id: effect.quarantine.structural-pregate status: draft - class: core + class: effect-gated statement: >- Decidable structural gates — payload binding, wire decode, allow-list — MUST run before quarantine admission; their failure MUST reject the @@ -198,7 +198,7 @@ rules: - id: effect.verdict.mapping status: draft - class: core + class: effect-gated statement: >- Adjacent FAILED MUST map to DENIED; adjacent or full-evidence INDETERMINATE MUST map to bounded quarantine; a required scope FAILED @@ -214,7 +214,7 @@ rules: - id: effect.expiry.deny-expired status: draft - class: core + class: effect-gated statement: >- Quarantine TTL or attempt exhaustion MUST NOT alter the evidence verdict: the record keeps evidence INDETERMINATE and the policy decision @@ -234,7 +234,7 @@ rules: - id: effect.dispatch.single-flight status: draft - class: core + class: effect-gated statement: >- State transitions MUST be protected by compare-and-swap or generation fencing so that concurrent release workers MUST NOT dispatch the same @@ -249,7 +249,7 @@ rules: - id: effect.state.confirmed-only status: draft - class: core + class: effect-gated statement: >- A local write attempt MUST NOT be recorded as release success; the only success state is EFFECT_CONFIRMED, a pre-existing effect-complete @@ -266,7 +266,7 @@ rules: - id: effect.state.unknown-no-retry status: draft - class: core + class: effect-gated statement: >- An ambiguous external write MUST transition to EFFECT_UNKNOWN; it MUST NOT be guessed into failure, and automatic retry from EFFECT_UNKNOWN @@ -282,7 +282,7 @@ rules: - id: effect.writer.capability status: draft - class: core + class: effect-gated statement: >- An external-effect-sink@1 writer MUST provide at least one of: put-if-absent on an idempotency key, a keyed command with effect @@ -299,7 +299,7 @@ rules: - id: effect.writer.limited-profile status: draft - class: core + class: effect-gated statement: >- A writer lacking every idempotency capability MUST be declared a limited profile; after EFFECT_UNKNOWN it MUST NOT retry automatically @@ -314,7 +314,7 @@ rules: - id: effect.delivery.identity status: draft - class: core + class: effect-gated statement: >- A delivery identity MUST include at least the sink contract, sink instance, target namespace, quarantine entry or delivery occurrence, @@ -330,7 +330,7 @@ rules: - id: effect.claim.no-local-exactly-once status: draft - class: core + class: effect-gated statement: >- Local sink state alone MUST NOT ground an EXACTLY_ONCE or an end-to-end AT_MOST_ONCE claim; those are independent delivery claims and MUST be @@ -346,7 +346,7 @@ rules: - id: effect.receipt.confirmed-only status: draft - class: core + class: effect-gated statement: >- A delivery receipt MUST be issued only after EFFECT_CONFIRMED and MUST bind the delivery identity, the ReleaseAuthorization digest, and the @@ -361,7 +361,7 @@ rules: - id: effect.receipt.limited-claim status: draft - class: core + class: effect-gated statement: >- A receipt MUST claim only that the sink observed an effect confirmation; it MUST NOT be promoted to target-side truth, @@ -376,7 +376,7 @@ rules: - id: effect.archive.observation-record status: draft - class: core + class: effect-gated statement: >- Pre-verification archival MUST write an append-only ObservationRecord carrying the exact variant and payload with coverage NOT_EVALUATED; an @@ -392,7 +392,7 @@ rules: - id: effect.archive.decision-record status: draft - class: core + class: effect-gated statement: >- An actionable consumer MUST read archived data only through a DecisionRecord whose decision is RELEASE, referencing the @@ -408,7 +408,7 @@ rules: - id: effect.quarantine.capacity status: draft - class: core + class: effect-gated statement: >- A quarantine store MUST enforce global and per-principal byte quotas, retention, backpressure, disk alerts, crash recovery, and an @@ -424,7 +424,7 @@ rules: - id: effect.quarantine.no-fallback status: draft - class: core + class: effect-gated statement: >- Quarantine capacity exhaustion MUST surface as rejection or backpressure; it MUST NOT fall back to an unverified external write. @@ -438,7 +438,7 @@ rules: - id: effect.migration.exclusive-writers status: draft - class: core + class: effect-gated statement: >- A legacy synchronous writer and the release worker MUST NOT be enabled for the same delivery target concurrently; the cutover feature gate @@ -454,7 +454,7 @@ rules: - id: effect.transport.possible-loss status: draft - class: core + class: effect-gated statement: >- While the inbound transport can drop a delivery before the quarantine entry is durably committed, the deployment MUST surface that window as diff --git a/tools/lint.py b/tools/lint.py index eea3aa6..45e8502 100644 --- a/tools/lint.py +++ b/tools/lint.py @@ -23,7 +23,7 @@ RFC2119 = re.compile(r"\b(?:MUST|SHALL|SHOULD|REQUIRED|RECOMMENDED|OPTIONAL|MAY)\b") ID_RE = re.compile(r"^[a-z0-9]+(\.[a-z0-9-]+){1,3}$") STATUSES = {"todo", "draft", "stable"} -CLASSES = {"core", "audit-reachable"} +CLASSES = {"core", "audit-reachable", "effect-gated"} MD_EXCLUDE = {"STATUS.md"} # 0.1 タグの vector カバレッジ床 (VERSIONING.md「0.1 タグの条件」と対)。 # 列挙された family は全 rule が 1 本以上の vector を持たなければならない。 diff --git a/tools/rule.schema.json b/tools/rule.schema.json index 363043a..4d77570 100644 --- a/tools/rule.schema.json +++ b/tools/rule.schema.json @@ -22,7 +22,7 @@ "pattern": "^[a-z0-9]+(\\.[a-z0-9-]+){1,3}$" }, "status": { "enum": ["todo", "draft", "stable"] }, - "class": { "enum": ["core", "audit-reachable"] }, + "class": { "enum": ["core", "audit-reachable", "effect-gated"] }, "statement": { "type": ["string", "null"] }, "uses": { "type": "array", "items": { "type": "string" } }, "schemas": { "type": "array", "items": { "type": "string" } }, diff --git a/tools/verify_evidence_vectors.py b/tools/verify_evidence_vectors.py new file mode 100644 index 0000000..461a1d7 --- /dev/null +++ b/tools/verify_evidence_vectors.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Independent EvidenceView identity and decision-vector checker. + +This implementation intentionally shares no code with provin.oss. The +fixtures contain no JSON numbers and only ASCII object keys, so Python's +sorted, compact JSON projection is byte-identical to RFC 8785 for this bounded +manifest contract. The checker rejects values outside that bounded domain +instead of pretending to be a general-purpose JCS implementation. +""" + +from __future__ import annotations + +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +VERSIONED = re.compile(r".+@[0-9]+$") +CONTENT_ADDRESS = re.compile(r"sha256:[0-9a-f]{64}$") + + +def reject_duplicate_keys(pairs): + out = {} + for key, value in pairs: + if key in out: + raise ValueError(f"duplicate object key {key!r}") + out[key] = value + return out + + +def load(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(), object_pairs_hook=reject_duplicate_keys) + + +def bounded_jcs(value: Any) -> bytes: + def check(node: Any) -> None: + if node is None or isinstance(node, (str, bool)): + return + if isinstance(node, (int, float)): + raise ValueError("numeric manifest members require a full RFC 8785 implementation") + if isinstance(node, list): + for item in node: + check(item) + return + if isinstance(node, dict): + for key, item in node.items(): + if not isinstance(key, str) or any(ord(ch) > 0xFFFF for ch in key): + raise ValueError("manifest keys must be BMP strings in this bounded implementation") + check(item) + return + raise ValueError(f"unsupported JSON value {type(node).__name__}") + + check(value) + return json.dumps( + value, + ensure_ascii=False, + allow_nan=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def evidence_view_id(manifest: dict[str, Any]) -> tuple[str, str]: + canonical = bounded_jcs(manifest) + return canonical.decode("utf-8"), "sha256:" + hashlib.sha256(canonical).hexdigest() + + +def decide(profile_id: str, required: list[str], vector: list[dict[str, Any]]) -> str: + if not VERSIONED.fullmatch(profile_id) or not required or len(required) != len(set(required)): + raise ValueError("invalid decision profile") + by_scope: dict[str, dict[str, Any]] = {} + for entry in vector: + scope = entry.get("scope") + coverage = entry.get("coverage") + has_truth = "truthState" in entry + if not isinstance(scope, str) or scope in by_scope: + raise ValueError("missing or duplicate scope") + if coverage not in {"EVALUATED", "NOT_EVALUATED", "UNSUPPORTED"}: + raise ValueError("invalid coverage") + if has_truth != (coverage == "EVALUATED"): + raise ValueError("truthState/coverage shape mismatch") + if has_truth and entry["truthState"] not in {"FAILED", "INDETERMINATE", "VERIFIED"}: + raise ValueError("invalid truthState") + by_scope[scope] = entry + + selected = [by_scope.get(scope) for scope in required] + if any(entry and entry.get("truthState") == "FAILED" for entry in selected): + return "DENY" + if any( + entry is None + or entry.get("coverage") != "EVALUATED" + or entry.get("truthState") != "VERIFIED" + for entry in selected + ): + return "QUARANTINE" + return "ACCEPT" + + +def main() -> int: + errors: list[str] = [] + + identity = load(ROOT / "vectors" / "evidence-view-001.json") + canonical, view_id = evidence_view_id(identity["input"]["credential"]["manifest"]) + if identity["expect"] != {"canonical": canonical, "evidenceViewId": view_id}: + errors.append("evidence-view-001 expected canonical bytes or digest drifted") + + mismatch = load(ROOT / "vectors" / "evidence-view-002.json") + supplied = mismatch["input"]["credential"]["evidenceViewId"] + _, derived = evidence_view_id(mismatch["input"]["credential"]["manifest"]) + if supplied == derived or mismatch["expect"] != "reject": + errors.append("evidence-view-002 no longer proves ID mismatch rejection") + + for name in ("claims-coverage-003", "claims-policy-001"): + vector = load(ROOT / "vectors" / f"{name}.json") + inp = vector["input"] + actual = decide(inp["decisionProfileId"], inp["requiredScopes"], inp["vector"]) + expected = vector["expect"] + if "decision" in expected and actual != expected["decision"]: + errors.append(f"{name}: {actual} != {expected['decision']}") + if "decisionNot" in expected and actual == expected["decisionNot"]: + errors.append(f"{name}: unexpectedly produced {actual}") + + malformed = ["claims-coverage-002", "claims-coverage-004"] + for name in malformed: + vector = load(ROOT / "vectors" / f"{name}.json") + entries = vector["input"]["credential"]["vector"] + try: + decide("shape-check@1", [entries[0]["scope"]], entries) + except ValueError: + continue + errors.append(f"{name}: malformed coverage vector was accepted") + + if errors: + for error in errors: + print(f"FAIL {error}") + return 1 + print("OK — EvidenceView identity and appraisal vectors verified independently") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vectors/evidence-view-001.json b/vectors/evidence-view-001.json new file mode 100644 index 0000000..064c83b --- /dev/null +++ b/vectors/evidence-view-001.json @@ -0,0 +1,42 @@ +{ + "id": "evidence-view-001", + "rule": "evidence.manifest.identity", + "description": "The EvidenceViewID is the sha256 content address of the RFC 8785 canonical EvaluationViewManifest and is independent of vector and decision fields.", + "input": { + "credential": { + "manifest": { + "head": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "spine": [ + { + "bodyAddress": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "wireVariantId": "wire:v1:jcs-rfc8785:sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + { + "bodyAddress": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "wireVariantId": "wire:v1:jcs-rfc8785:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + ], + "claimContractId": "linear-provenance@1", + "canonicalizerId": "jcs-rfc8785", + "cryptosuiteId": "W3C_EDDSA_JCS_2022_REC_20250515@1", + "schemaVersion": "pipeline-pass-credential@1", + "inputSnapshotDigests": { + "did:did:dplaax:factory-a": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "lifecycle:factory-a": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + } + }, + "evidenceViewId": "sha256:4382e8875ce5c79e0f053215993f901331003e89d125de78bc610dbdb90b06aa", + "vector": [ + { + "scope": "LINEAR_ATTESTATION@1", + "coverage": "EVALUATED", + "truthState": "VERIFIED" + } + ] + } + }, + "expect": { + "canonical": "{\"canonicalizerId\":\"jcs-rfc8785\",\"claimContractId\":\"linear-provenance@1\",\"cryptosuiteId\":\"W3C_EDDSA_JCS_2022_REC_20250515@1\",\"head\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"inputSnapshotDigests\":{\"did:did:dplaax:factory-a\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"lifecycle:factory-a\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"},\"schemaVersion\":\"pipeline-pass-credential@1\",\"spine\":[{\"bodyAddress\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\",\"wireVariantId\":\"wire:v1:jcs-rfc8785:sha256:2222222222222222222222222222222222222222222222222222222222222222\"},{\"bodyAddress\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"wireVariantId\":\"wire:v1:jcs-rfc8785:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"}]}", + "evidenceViewId": "sha256:4382e8875ce5c79e0f053215993f901331003e89d125de78bc610dbdb90b06aa" + } +} diff --git a/vectors/evidence-view-002.json b/vectors/evidence-view-002.json new file mode 100644 index 0000000..22aad8c --- /dev/null +++ b/vectors/evidence-view-002.json @@ -0,0 +1,34 @@ +{ + "id": "evidence-view-002", + "rule": "evidence.manifest.identity", + "description": "A supplied EvidenceViewID that does not equal the canonical manifest digest is rejected even when the manifest and scoped vector are structurally valid.", + "input": { + "credential": { + "manifest": { + "head": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "spine": [ + { + "bodyAddress": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "wireVariantId": "wire:v1:jcs-rfc8785:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + ], + "claimContractId": "linear-provenance@1", + "canonicalizerId": "jcs-rfc8785", + "cryptosuiteId": "W3C_EDDSA_JCS_2022_REC_20250515@1", + "schemaVersion": "pipeline-pass-credential@1", + "inputSnapshotDigests": { + "did": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + }, + "evidenceViewId": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "vector": [ + { + "scope": "LINEAR_ATTESTATION@1", + "coverage": "EVALUATED", + "truthState": "VERIFIED" + } + ] + } + }, + "expect": "reject" +}