diff --git a/CHANGELOG.md b/CHANGELOG.md index 6479cd7..c515b55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this specification are documented here. The spec document follows [Semantic Versioning](https://semver.org/). Wire formats (Permit `v1`, `closure_v1`, `closure_v2`, chain entry `v1`) version independently and are not bumped by spec-document revisions. +## [1.4.0] — 2026-05-21 + +Adds Step 4 claim contracts for signed permit decisions, signed permit +revocations, and scope-faithful absence adjudication for post-revocation +dispatch initiation. Permit wire format `v1`, closure formats, and chain entry +`v1` are unchanged. + +- Add Step 4 claim contracts (`permit.decision.v1`, `permit.revoked.v1`, `permit.dispatch_absence_after_revocation.v1`) per multi-model design pass 2026-05-21. Scope-faithful absence adjudication, falsifiability-oriented trust model. Reserved `non_membership_profile` namespace for future SMT/NMT native non-membership. PR 1 of Step 4; emission + adjudication land in PR 2 + PR 3. + ## [1.3.1] — 2026-05-20 Adds promoted scope-faithfulness negative and edge corpus fixtures for the public verifier contract. Permit wire format `v1`, closure formats, chain entry `v1`, claim registry, and semantic artifacts are unchanged. diff --git a/README.md b/README.md index 02cc062..dab2a44 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License](https://img.shields.io/github/license/keelapi/keel-permit)](LICENSE) [![Latest release](https://img.shields.io/github/v/release/keelapi/keel-permit?label=release)](https://github.com/keelapi/keel-permit/releases) -![Spec version](https://img.shields.io/badge/spec-1.3.1-blue) +![Spec version](https://img.shields.io/badge/spec-1.4.0-blue) A pre-execution decision record for AI agent systems. A Permit records that an action was evaluated and decided, and, for dispatched allow executions, can be bound to the final provider or tool request. A Permit JSON object alone is not self-authenticating; verification is performed from signed export artifacts and the relevant public keys or key manifest. Those artifacts are verifiable without contacting the issuer when the verifier has the signed export artifacts and relevant public keys/key manifest. @@ -12,7 +12,7 @@ This repository contains the wire-format specification, JSON schemas, and verifi | | | |---|---| -| Spec document version | 1.3.1 | +| Spec document version | 1.4.0 | | Permit wire format | `v1` | | Closure record format | `closure_v1`, `closure_v2` | | Chain entry hash format | `v1` | @@ -61,6 +61,8 @@ flowchart LR - [`spec/permit-chain-v1.md`](spec/permit-chain-v1.md) — Delegated Permit Chain semantics - [`spec/verifier-claims-v0.md`](spec/verifier-claims-v0.md) — Verifier claim model - [`spec/verifier-pack-pinning-v0.md`](spec/verifier-pack-pinning-v0.md) — Pinned semantics for evidence packs +- [`spec/permit-revoked-event-v1.md`](spec/permit-revoked-event-v1.md) — Signed permit revocation event +- [`spec/dispatch-absence-after-revocation-v1.md`](spec/dispatch-absence-after-revocation-v1.md) — Scope-faithful absence adjudication after revocation ## Schemas diff --git a/claim_registry/v0.json b/claim_registry/v0.json index d34b766..c88a1c5 100644 --- a/claim_registry/v0.json +++ b/claim_registry/v0.json @@ -208,6 +208,39 @@ "insufficient_evidence", "unverifiable_scope" ] + }, + { + "name": "permit.decision.v1", + "assertion": "The issuance-time permit decision is validly signed by the permit-binding key through the v1 binding canonical payload, including the canonical `decision`; this claim adjudicates the signed issuance decision only and never mutated row state.", + "required_evidence": "Permit decision evidence carrying the v1 binding canonical payload or fields sufficient to reconstruct it, binding_canonical_hash, binding_signature, binding_key_id, binding_issued_at, permit-binding trust root, and pinned permit decision semantics.", + "verdict_enum": [ + "supported", + "disproved", + "insufficient_evidence", + "unverifiable_scope" + ] + }, + { + "name": "permit.revoked.v1", + "assertion": "A new-only post-cutover `permit.revoked` event is validly signed by the permit-binding key, is bound to both `permit_id` and `project_id`, and uses v1 immediate-effect semantics where `effective_at` equals `revoked_at`.", + "required_evidence": "Strict permit.revoked event payload, signature, permit-binding trust root, referenced permit/project identity, signing time or event time for key-window resolution, and pinned revoked-event semantics.", + "verdict_enum": [ + "supported", + "disproved", + "insufficient_evidence", + "unverifiable_scope" + ] + }, + { + "name": "permit.dispatch_absence_after_revocation.v1", + "assertion": "Scope-faithful absence adjudication shows that no `dispatch.egress_bound` event appears for the revoked permit and project in the declared scope where `occurred_at` is greater than or equal to the revocation `effective_at` and less than the checkpoint boundary.", + "required_evidence": "supported permit.revoked.v1 evidence; signed export scope-faithfulness segment using scope-predicate v1 equality on project_id, permit_id, and event_type=dispatch.egress_bound plus bounded occurred_at range; checkpoint.scope_state.v1 sidecar; referenced checkpoint JSON; bridge/proof records; pinned absence, export scope-faithfulness, scope-state, and governance-chain semantics.", + "verdict_enum": [ + "supported", + "disproved", + "insufficient_evidence", + "unverifiable_scope" + ] } ] } diff --git a/schemas/permit-revoked-event.schema.json b/schemas/permit-revoked-event.schema.json new file mode 100644 index 0000000..a7ad9a5 --- /dev/null +++ b/schemas/permit-revoked-event.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/keelapi/keel-permit/blob/main/schemas/permit-revoked-event.schema.json", + "title": "Permit Revoked Event v1", + "description": "Strict payload shape for a post-cutover signed permit.revoked event. The normative v1 rule effective_at == revoked_at is specified in spec/permit-revoked-event-v1.md.", + "type": "object", + "additionalProperties": false, + "properties": { + "permit_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the revoked permit." + }, + "project_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the project that owns the revoked permit. Signed to prevent cross-project replay." + }, + "actor_id": { + "type": "string", + "format": "uuid", + "description": "Opaque actor UUID. Email, name, display labels, and customer-controlled identifiers are not valid actor_id values." + }, + "actor_kind": { + "type": "string", + "enum": [ + "user", + "service_account", + "system", + "api_key" + ], + "description": "Opaque actor class for the revocation actor." + }, + "reason_code": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$", + "description": "Machine-readable taxonomy code. Free-text reasons are not part of the signed v1 payload." + }, + "revoked_at": { + "type": "string", + "format": "date-time", + "description": "Governance event time for the revocation." + }, + "effective_at": { + "type": "string", + "format": "date-time", + "description": "Effective revocation time. v1 requires this value to equal revoked_at." + }, + "signature": { + "type": "string", + "pattern": "^[A-Za-z0-9+/]{86}==$", + "description": "Base64 Ed25519 signature over the revocation event canonical hash using a permit-binding key." + } + }, + "required": [ + "permit_id", + "project_id", + "actor_id", + "actor_kind", + "reason_code", + "revoked_at", + "effective_at", + "signature" + ] +} diff --git a/scripts/build_step4_permit_claim_fixtures.mjs b/scripts/build_step4_permit_claim_fixtures.mjs new file mode 100644 index 0000000..478e40d --- /dev/null +++ b/scripts/build_step4_permit_claim_fixtures.mjs @@ -0,0 +1,747 @@ +#!/usr/bin/env node +// Build deterministic Step 4 permit-claim baseline fixtures. + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const CORPUS_ROOT = path.join(ROOT, "test-vectors", "verifier_claims", "v0"); +const FIXTURE_ROOT = path.join(CORPUS_ROOT, "fixtures"); +const TRUST_ROOT = path.join(CORPUS_ROOT, "trust_roots", "step4-permit-claims-trust-root.json"); + +const PROJECT_ID = "00000000-0000-0000-0000-000000000041"; +const CHAIN_SCOPE = `project:${PROJECT_ID}`; +const GENESIS_PREV_HASH = "0".repeat(64); +const SIGNED_AT = "2026-05-21T10:00:00.000000Z"; +const REVOCATION_TIME = "2026-05-21T10:05:00.000000Z"; +const CHECKPOINT_BOUNDARY = "2026-05-21T10:10:00.000000Z"; +const CHECKPOINT_ID = "40000000-0000-4000-8000-000000000001"; +const EMPTY_TREE_HASH = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + +const EXPORT_SEED = Buffer.alloc(32, 0x11); +const CHECKPOINT_SEED = Buffer.alloc(32, 0x22); +const PERMIT_BINDING_SEED = Buffer.alloc(32, 0x33); + +const DECISION_FIXTURES = [ + { + id: "permit-decision-allow-valid", + permitId: "10000000-0000-4000-8000-000000000001", + decision: "allow", + title: "Valid signed permit allow decision" + }, + { + id: "permit-decision-deny-valid", + permitId: "10000000-0000-4000-8000-000000000002", + decision: "deny", + title: "Valid signed permit deny decision" + }, + { + id: "permit-decision-challenge-valid", + permitId: "10000000-0000-4000-8000-000000000003", + decision: "challenge", + title: "Valid signed permit challenge decision" + } +]; + +const ABSENCE_PERMIT_ID = "20000000-0000-4000-8000-000000000001"; +const ACTOR_ID = "30000000-0000-4000-8000-000000000001"; + +const SUPPORTED_PREDICATE_KINDS = [ + "project_id", + "permit_id", + "request_id", + "event_type", + "category", + "severity", + "decision_type", + "policy_id", + "provider", + "sequence_number", + "created_at", + "occurred_at", + "section", + "export_type" +]; + +function sortDeep(value) { + if (Array.isArray(value)) { + return value.map(sortDeep); + } + if (value && typeof value === "object" && value.constructor === Object) { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortDeep(value[key])]) + ); + } + return value; +} + +function canonicalJson(value) { + return JSON.stringify(sortDeep(value)); +} + +function prettyJson(value) { + return `${JSON.stringify(sortDeep(value), null, 2)}\n`; +} + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, prettyJson(value), "utf8"); +} + +function writeText(filePath, value) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, value, "utf8"); +} + +function sha256Hex(input) { + return crypto.createHash("sha256").update(input).digest("hex"); +} + +function sha256Prefixed(input) { + return `sha256:${sha256Hex(input)}`; +} + +function ed25519PrivateKey(seed) { + const prefix = Buffer.from("302e020100300506032b657004220420", "hex"); + return crypto.createPrivateKey({ key: Buffer.concat([prefix, seed]), format: "der", type: "pkcs8" }); +} + +function rawPublicKey(privateKey) { + const spki = crypto.createPublicKey(privateKey).export({ format: "der", type: "spki" }); + return Buffer.from(spki).subarray(-32); +} + +function publicKeyB64(privateKey) { + return `ed25519:${rawPublicKey(privateKey).toString("base64")}`; +} + +function prefixedKeyId(privateKey) { + return `sha256:${sha256Hex(rawPublicKey(privateKey)).slice(0, 32)}`; +} + +function runtimeKeyId(privateKey) { + return sha256Hex(rawPublicKey(privateKey)).slice(0, 16); +} + +function signB64(privateKey, message) { + const bytes = Buffer.isBuffer(message) ? message : Buffer.from(String(message), "utf8"); + return crypto.sign(null, bytes, privateKey).toString("base64"); +} + +function signEd25519(privateKey, message) { + return `ed25519:${signB64(privateKey, message)}`; +} + +function fileRef(relativePath, semanticId) { + const bytes = fs.readFileSync(path.join(ROOT, relativePath)); + return { + id: semanticId, + hash: sha256Prefixed(bytes), + path: relativePath + }; +} + +function registryRef() { + return fileRef("claim_registry/v0.json", "keel.verifier_claim_registry.v0"); +} + +function claimSet(claims) { + return { + version: "verifier-claims.v0", + registry: registryRef(), + claims: claims.map((name) => ({ name, required: true })) + }; +} + +function semanticsPins(artifacts) { + return { + version: "keel-semantics-pins.v0", + mode: "pinned", + artifacts + }; +} + +const SEMANTICS = { + exportManifest: fileRef("semantics/export_manifest/integrity_v1.json", "keel.export_manifest.integrity.v1"), + governanceChain: fileRef("semantics/governance_chain/record_hash_v1.json", "keel.governance_chain.record_hash.v1"), + checkpointComposite: fileRef("semantics/checkpoint/composite_hash_v1.json", "keel.checkpoint.composite_hash.v1"), + checkpointSignature: fileRef("semantics/checkpoint/signature_v1.json", "keel.checkpoint.signature.v1"), + scopeMerkle: fileRef("semantics/scope_state/merkle_v1.json", "keel.scope_state.merkle.v1"), + scopeSidecar: fileRef("semantics/scope_state/sidecar_format_v1.json", "keel.scope_state.sidecar_format.v1"), + exportScopeFaithfulness: fileRef("semantics/export/scope_faithfulness_v1.json", "keel.export.scope_faithfulness.v1"), + permitDecision: fileRef("semantics/permit/decision_v1.json", "keel.permit.decision.v1"), + permitRevoked: fileRef("semantics/permit/revoked_event_v1.json", "keel.permit.revoked_event.v1"), + permitDispatchAbsence: fileRef( + "semantics/permit/dispatch_absence_after_revocation_v1.json", + "keel.permit.dispatch_absence_after_revocation.v1" + ) +}; + +function buildTrustRoot() { + const exportKey = ed25519PrivateKey(EXPORT_SEED); + const checkpointKey = ed25519PrivateKey(CHECKPOINT_SEED); + const permitKey = ed25519PrivateKey(PERMIT_BINDING_SEED); + writeJson(TRUST_ROOT, { + schema_version: 1, + generated_at: SIGNED_AT, + keys: [ + { + algorithm: "ed25519", + key_id: prefixedKeyId(exportKey), + public_key: publicKeyB64(exportKey), + purpose: "export_signing", + status: "active", + valid_from: "2026-01-01T00:00:00Z", + valid_to: null + }, + { + algorithm: "ed25519", + key_id: runtimeKeyId(permitKey), + public_key: publicKeyB64(permitKey), + purpose: "permit_binding_signing", + status: "active", + valid_from: "2026-01-01T00:00:00Z", + valid_to: null + }, + { + algorithm: "ed25519", + key_id: prefixedKeyId(checkpointKey), + public_key: publicKeyB64(checkpointKey), + purpose: "integrity_checkpoint", + status: "active", + valid_from: "2026-01-01T00:00:00Z", + valid_to: null + }, + { + algorithm: "ed25519", + key_id: prefixedKeyId(checkpointKey), + public_key: publicKeyB64(checkpointKey), + purpose: "scope_state", + status: "active", + valid_from: "2026-01-01T00:00:00Z", + valid_to: null + } + ] + }); +} + +function decisionCanonicalPayload(spec) { + return { + binding_version: "v1", + permit_id: spec.permitId, + project_id: PROJECT_ID, + parent_permit_id: null, + decision: spec.decision, + reason: `Fixture ${spec.decision} decision.`, + provider: "openai", + model: "gpt-4o-mini", + operation: "responses.create", + action_name: "chat.completion", + request_fingerprint: sha256Hex(`${spec.id}:request`), + constraints: { + max_output_tokens: 512, + temperature_max: 1 + }, + routing: { + requested_provider: "openai", + requested_model: "gpt-4o-mini", + selected_provider: "openai", + selected_model: "gpt-4o-mini", + fallback_chain: [], + fallback_occurred: false, + reason_code: "fixture.primary", + reason_metadata: {} + }, + policy_id: "policy:step4-fixture", + policy_version: "v2026-05-21", + policy_snapshot_hash: sha256Hex(`${spec.id}:policy`), + issued_at: SIGNED_AT, + expires_at: "2026-05-21T11:00:00.000000Z", + is_dry_run: false, + binding_key_id: runtimeKeyId(ed25519PrivateKey(PERMIT_BINDING_SEED)), + final_request_hash: null + }; +} + +function decisionEvidence(spec) { + const permitKey = ed25519PrivateKey(PERMIT_BINDING_SEED); + const payload = decisionCanonicalPayload(spec); + const hash = sha256Hex(canonicalJson(payload)); + return { + artifact_type: "permit_decision_binding", + artifact_version: "permit.decision.v1", + canonical_payload: payload, + binding_canonical_hash: hash, + binding_signature: signEd25519(permitKey, hash), + binding_key_id: runtimeKeyId(permitKey), + binding_issued_at: SIGNED_AT, + expected_decision: spec.decision + }; +} + +function revokedEvent({ permitId = ABSENCE_PERMIT_ID, projectId = PROJECT_ID } = {}) { + const permitKey = ed25519PrivateKey(PERMIT_BINDING_SEED); + const payload = { + permit_id: permitId, + project_id: projectId, + actor_id: ACTOR_ID, + actor_kind: "user", + reason_code: "operator.requested", + revoked_at: REVOCATION_TIME, + effective_at: REVOCATION_TIME + }; + const hash = sha256Hex(canonicalJson(payload)); + return { + event: { + ...payload, + signature: signB64(permitKey, hash) + }, + canonical_hash: hash + }; +} + +function buildManifest({ fixtureId, exportPayload, claims, artifacts, recordCount }) { + const exportKey = ed25519PrivateKey(EXPORT_SEED); + const exportBytes = Buffer.from(prettyJson(exportPayload), "utf8"); + const contentHash = sha256Prefixed(exportBytes); + return { + export_id: fixtureId, + project_id: PROJECT_ID, + export_type: "audit_export", + format: "json", + compressed: false, + record_count: recordCount, + content_hash: contentHash, + key_id: prefixedKeyId(exportKey), + public_key: publicKeyB64(exportKey), + signed_at: SIGNED_AT, + claim_set: claimSet(["export.integrity.v1", ...claims]), + semantics_pins: semanticsPins([SEMANTICS.exportManifest, ...artifacts]), + signature: signEd25519(exportKey, contentHash) + }; +} + +function writeReadme(fixtureId, title, body) { + writeText( + path.join(FIXTURE_ROOT, fixtureId, "README.md"), + `# ${fixtureId}\n\n${title}.\n\n${body}\n` + ); +} + +function writeDecisionFixture(spec) { + const fixtureDir = path.join(FIXTURE_ROOT, spec.id); + const evidence = decisionEvidence(spec); + const exportPayload = { + schema: "keel.step4.permit_claim_fixture/v1", + fixture_id: spec.id, + project_id: PROJECT_ID, + permit_decision: evidence + }; + const manifest = buildManifest({ + fixtureId: spec.id, + exportPayload, + claims: ["permit.decision.v1"], + artifacts: [SEMANTICS.permitDecision], + recordCount: 1 + }); + writeJson(path.join(fixtureDir, "pack", "export.json"), exportPayload); + writeJson(path.join(fixtureDir, "pack", "manifest.json"), manifest); + writeReadme( + spec.id, + spec.title, + `## What It Tests\n\nThe pack contains a signed issuance-time permit decision binding whose canonical payload decision is \`${spec.decision}\`.\n\n## Expected Verdict\n\n\`permit.decision.v1\` is expected to return \`supported\`.` + ); + return corpusExportRecord({ + id: spec.id, + title: spec.title, + claims: ["export.integrity.v1", "permit.decision.v1"], + features: ["step4_permit_claims"], + extraPack: {} + }); +} + +function recordHashV1(entry) { + const timestamp = String(entry.created_at).replace(/Z$/, ""); + const parts = [ + entry.event_id, + entry.event_type, + entry.resource_type || "", + entry.resource_id || "", + entry.outcome || "", + entry.severity, + timestamp, + entry.prev_hash, + String(entry.sequence_number) + ]; + return sha256Hex(parts.join("|")); +} + +function chainEntry({ eventId, eventType, sequenceNumber, prevHash, occurredAt, payloadJson }) { + const entry = { + event_id: eventId, + event_type: eventType, + project_id: PROJECT_ID, + permit_id: ABSENCE_PERMIT_ID, + resource_type: "permit", + resource_id: ABSENCE_PERMIT_ID, + outcome: "success", + severity: "info", + occurred_at: occurredAt, + created_at: occurredAt, + payload_json: payloadJson, + chain_scope: CHAIN_SCOPE, + sequence_number: sequenceNumber, + prev_hash: prevHash, + chain_format_version: "v1" + }; + entry.record_hash = recordHashV1(entry); + return entry; +} + +function buildAbsenceChain({ includePreRevocationDispatch }) { + const entries = []; + let prev = GENESIS_PREV_HASH; + let sequence = 1; + if (includePreRevocationDispatch) { + const dispatch = chainEntry({ + eventId: "evt_step4_pre_revocation_dispatch", + eventType: "dispatch.egress_bound", + sequenceNumber: sequence, + prevHash: prev, + occurredAt: "2026-05-21T10:04:00.000000Z", + payloadJson: { + project_id: PROJECT_ID, + permit_id: ABSENCE_PERMIT_ID, + event_type: "dispatch.egress_bound", + occurred_at: "2026-05-21T10:04:00.000000Z", + dispatch_request_digest_v1: sha256Hex("pre-revocation-dispatch") + } + }); + entries.push(dispatch); + prev = dispatch.record_hash; + sequence += 1; + } + const revocation = revokedEvent(); + const revoked = chainEntry({ + eventId: "evt_step4_permit_revoked", + eventType: "permit.revoked", + sequenceNumber: sequence, + prevHash: prev, + occurredAt: REVOCATION_TIME, + payloadJson: { + project_id: PROJECT_ID, + permit_id: ABSENCE_PERMIT_ID, + event_type: "permit.revoked", + occurred_at: REVOCATION_TIME, + revoked_at: REVOCATION_TIME, + effective_at: REVOCATION_TIME, + reason_code: "operator.requested" + } + }); + entries.push(revoked); + return { entries, revocation }; +} + +function compositeHash(chainHeads) { + const lines = Object.keys(chainHeads) + .sort() + .map((scope) => `${scope}:${chainHeads[scope].sequence_number}:${chainHeads[scope].last_record_hash}`); + return sha256Prefixed(Buffer.from(lines.join("\n"), "utf8")); +} + +function buildCheckpoint(entries) { + const checkpointKey = ed25519PrivateKey(CHECKPOINT_SEED); + const last = entries.at(-1); + const chainHeads = { + [CHAIN_SCOPE]: { + sequence_number: last.sequence_number, + last_record_hash: last.record_hash + } + }; + const composite = compositeHash(chainHeads); + return { + checkpoint_id: CHECKPOINT_ID, + computed_at: CHECKPOINT_BOUNDARY, + keel_version: "step4-permit-claims-fixtures", + chain_heads: chainHeads, + composite_hash: composite, + key_id: prefixedKeyId(checkpointKey), + public_key: publicKeyB64(checkpointKey), + claim_set: claimSet(["checkpoint.composite_hash.v1", "checkpoint.signature.v1"]), + semantics_pins: semanticsPins([SEMANTICS.checkpointComposite, SEMANTICS.checkpointSignature]), + signature: signEd25519(checkpointKey, composite) + }; +} + +function chainScopeHash() { + return sha256Hex(canonicalJson({ chain_scope: CHAIN_SCOPE })); +} + +function absencePredicate() { + return { + version: "keel.scope_predicate.v1", + operator: "and", + equals: { + project_id: PROJECT_ID, + permit_id: ABSENCE_PERMIT_ID, + event_type: "dispatch.egress_bound" + }, + ranges: { + occurred_at: { + gte: REVOCATION_TIME, + lt: CHECKPOINT_BOUNDARY + } + } + }; +} + +function buildSidecar({ entries, predicate }) { + const checkpointKey = ed25519PrivateKey(CHECKPOINT_SEED); + const predicateHash = sha256Prefixed(Buffer.from(canonicalJson(predicate), "utf8")); + const sidecar = { + artifact_type: "checkpoint_scope_state", + version: "checkpoint_scope_state.v1", + scope_state_id: `keel.scope_state.v1:${chainScopeHash()}:${CHECKPOINT_ID}`, + checkpoint_id: CHECKPOINT_ID, + chain_scope: CHAIN_SCOPE, + predicate_grammar_version: "keel.scope_predicate.v1", + predicate_basis: { + canonicalization_profile: "keel.canonical_json.payload.v1", + supported_predicate_kinds: SUPPORTED_PREDICATE_KINDS, + reserved_namespaces: ["keel.scope_predicate.reserved.v1", "non_membership_profile"] + }, + commitment_profile: "keel.scope_state.merkle.v1", + scope_commitments: [ + { + predicate_value: predicate, + predicate_value_hash: predicateHash, + first_matching_sequence: null, + last_matching_sequence: null, + matching_count: 0, + membership_root_hash: EMPTY_TREE_HASH + } + ], + tree_size: entries.at(-1).sequence_number, + signed_at: SIGNED_AT, + signature: { + algorithm: "Ed25519", + key_id: prefixedKeyId(checkpointKey), + signature: "ed25519:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + trust_root_reference: { + manifest_version: "keel.public_key_manifest.v1", + purpose: "scope_state", + key_id: prefixedKeyId(checkpointKey) + } + }; + const signedPayload = structuredClone(sidecar); + delete signedPayload.signature.signature; + sidecar.signature.signature = signEd25519(checkpointKey, canonicalJson(signedPayload)); + return sidecar; +} + +function buildAbsenceFixture({ id, title, includePreRevocationDispatch }) { + const { entries, revocation } = buildAbsenceChain({ includePreRevocationDispatch }); + const checkpoint = buildCheckpoint(entries); + const predicate = absencePredicate(); + const sidecar = buildSidecar({ entries, predicate }); + const sidecarBytes = Buffer.from(prettyJson(sidecar), "utf8"); + const sidecarHash = sha256Prefixed(sidecarBytes); + const filtersHash = sha256Prefixed(Buffer.from(canonicalJson(predicate), "utf8")); + + const segment = { + segment_id: id, + declared_scope: { + version: "keel.scope_declaration.v1", + scope_kind: "declared_sample", + chain_scope: CHAIN_SCOPE, + population_label: "Post-revocation dispatch.egress_bound events for one permit", + predicate, + presentation_policy: { + version: "keel.presentation_policy.v1", + policy_kind: "none", + policy_parameters: {} + } + }, + declared_start: { + kind: "genesis", + chain_scope: CHAIN_SCOPE, + sequence_number: 1, + genesis_prev_hash: GENESIS_PREV_HASH + }, + declared_end: { + checkpoint_id: CHECKPOINT_ID, + chain_scope: CHAIN_SCOPE, + sequence_number: entries.at(-1).sequence_number, + last_record_hash: entries.at(-1).record_hash, + boundary_policy: "explicit_checkpoint", + checkpoint_boundary: CHECKPOINT_BOUNDARY + }, + scope_state_reference: { + artifact_type: "checkpoint_scope_state", + scope_state_id: sidecar.scope_state_id, + checkpoint_id: CHECKPOINT_ID, + chain_scope: CHAIN_SCOPE, + artifact_hash: sidecarHash, + storage_uri: `scope-state/v1/${chainScopeHash()}/${CHECKPOINT_ID}/checkpoint-scope-state-v1.json` + }, + canonical_filters: { + canonicalization_profile: "keel.canonical_json.payload.v1", + raw_filters: predicate, + filters_hash: filtersHash + }, + chain_evidence: { + disclosure_records: [], + proof_bridge_records: entries + } + }; + + const exportPayload = { + schema: "keel.step4.permit_claim_fixture/v1", + fixture_id: id, + project_id: PROJECT_ID, + permit_id: ABSENCE_PERMIT_ID, + revocation_event: revocation, + scope_faithfulness: { + version: "keel.export_scope_faithfulness.v1", + segments: [segment] + } + }; + const manifest = buildManifest({ + fixtureId: id, + exportPayload, + claims: [ + "permit.revoked.v1", + "checkpoint.scope_state.v1", + "export.scope_faithfulness.v1", + "permit.dispatch_absence_after_revocation.v1" + ], + artifacts: [ + SEMANTICS.permitRevoked, + SEMANTICS.checkpointComposite, + SEMANTICS.checkpointSignature, + SEMANTICS.governanceChain, + SEMANTICS.scopeMerkle, + SEMANTICS.scopeSidecar, + SEMANTICS.exportScopeFaithfulness, + SEMANTICS.permitDispatchAbsence + ], + recordCount: 0 + }); + + const fixtureDir = path.join(FIXTURE_ROOT, id); + writeJson(path.join(fixtureDir, "pack", "export.json"), exportPayload); + writeJson(path.join(fixtureDir, "pack", "manifest.json"), manifest); + writeJson(path.join(fixtureDir, "pack", "checkpoint.json"), checkpoint); + writeJson(path.join(fixtureDir, "sidecars", "checkpoint-scope-state-v1.json"), sidecar); + writeReadme( + id, + title, + `## What It Tests\n\nThe pack contains supported revocation evidence and a scope-faithful absence adjudication segment whose post-revocation \`dispatch.egress_bound\` matching count is zero.${includePreRevocationDispatch ? " A pre-revocation `dispatch.egress_bound` record is supplied as bridge evidence and does not match the bounded `occurred_at` range." : ""}\n\n## Expected Verdict\n\n\`permit.dispatch_absence_after_revocation.v1\` is expected to return \`supported\`.` + ); + return corpusExportRecord({ + id, + title, + claims: [ + "export.integrity.v1", + "permit.revoked.v1", + "checkpoint.scope_state.v1", + "export.scope_faithfulness.v1", + "permit.dispatch_absence_after_revocation.v1" + ], + features: ["scope_faithfulness", "step4_permit_claims"], + extraPack: { + checkpoint_file: `fixtures/${id}/pack/checkpoint.json`, + sidecar_file: `fixtures/${id}/sidecars/checkpoint-scope-state-v1.json` + } + }); +} + +function writeRevokedFixture() { + const id = "permit-revoked-valid"; + const title = "Valid signed permit revocation event"; + const exportPayload = { + schema: "keel.step4.permit_claim_fixture/v1", + fixture_id: id, + project_id: PROJECT_ID, + permit_id: ABSENCE_PERMIT_ID, + revocation_event: revokedEvent() + }; + const manifest = buildManifest({ + fixtureId: id, + exportPayload, + claims: ["permit.revoked.v1"], + artifacts: [SEMANTICS.permitRevoked], + recordCount: 1 + }); + const fixtureDir = path.join(FIXTURE_ROOT, id); + writeJson(path.join(fixtureDir, "pack", "export.json"), exportPayload); + writeJson(path.join(fixtureDir, "pack", "manifest.json"), manifest); + writeReadme( + id, + title, + "## What It Tests\n\nThe pack contains a signed `permit.revoked` event with `permit_id`, `project_id`, opaque actor identity, taxonomy `reason_code`, and `effective_at == revoked_at`.\n\n## Expected Verdict\n\n`permit.revoked.v1` is expected to return `supported`." + ); + return corpusExportRecord({ + id, + title, + claims: ["export.integrity.v1", "permit.revoked.v1"], + features: ["step4_permit_claims"], + extraPack: {} + }); +} + +function corpusExportRecord({ id, title, claims, features, extraPack }) { + return { + claims: claims.map((name) => ({ expected_verdict: "supported", name })), + expected_current: { + outcome: "PASS", + reason_classes: [] + }, + id, + kind: "export", + pack: { + export_file: `fixtures/${id}/pack/export.json`, + features, + key_manifest: "trust_roots/step4-permit-claims-trust-root.json", + manifest: `fixtures/${id}/pack/manifest.json`, + ...extraPack + }, + title + }; +} + +function updateCorpus(records) { + const corpusPath = path.join(CORPUS_ROOT, "corpus.json"); + const corpus = JSON.parse(fs.readFileSync(corpusPath, "utf8")); + const ids = new Set(records.map((record) => record.id)); + corpus.corpus_version = "verifier-claims-v0-golden-2026-05-21-step4"; + corpus.records = corpus.records.filter((record) => !ids.has(record.id)).concat(records); + writeJson(corpusPath, corpus); +} + +function main() { + buildTrustRoot(); + const records = [ + ...DECISION_FIXTURES.map(writeDecisionFixture), + writeRevokedFixture(), + buildAbsenceFixture({ + id: "dispatch-absence-after-revocation-valid-empty-scope", + title: "Valid post-revocation dispatch absence with empty matching scope", + includePreRevocationDispatch: false + }), + buildAbsenceFixture({ + id: "dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch", + title: "Valid post-revocation dispatch absence with pre-revocation dispatch evidence", + includePreRevocationDispatch: true + }) + ]; + updateCorpus(records); + console.log(`Generated ${records.length} Step 4 permit-claim fixtures.`); +} + +main(); diff --git a/semantics/permit/decision_v1.json b/semantics/permit/decision_v1.json new file mode 100644 index 0000000..de1bdec --- /dev/null +++ b/semantics/permit/decision_v1.json @@ -0,0 +1,61 @@ +{ + "id": "keel.permit.decision.v1", + "version": "v1", + "kind": "permit_decision_signature_recipe", + "status": "released", + "body": { + "claim": "permit.decision.v1", + "artifact_type": "permit_decision_binding", + "artifact_version": "permit.decision.v1", + "canonicalization_profile": "keel.canonical_json.payload.v1", + "binding_version": "v1", + "scope": "issuance_only", + "issuer_key_purpose": "permit_binding_signing", + "canonical_payload": { + "source": "The issuance-time v1 permit binding canonical payload, captured before any dispatch-time or lifecycle mutation.", + "required_fields": [ + "binding_version", + "permit_id", + "project_id", + "parent_permit_id", + "decision", + "reason", + "provider", + "model", + "operation", + "action_name", + "request_fingerprint", + "constraints", + "routing", + "policy_id", + "policy_version", + "policy_snapshot_hash", + "issued_at", + "expires_at", + "is_dry_run", + "binding_key_id", + "final_request_hash" + ], + "decision_rule": "canonical_payload.decision is the only decision adjudicated by this claim and must be one of allow, deny, or challenge after issuer v1 normalization.", + "identity_binding": "canonical_payload.permit_id and canonical_payload.project_id bind the signed decision to one permit and one project.", + "row_state_rule": "Verifiers MUST NOT adjudicate a later mutable permit row as the issuance decision unless the pack supplies evidence that the reconstructed payload is the original issuance-time signed payload." + }, + "hash_rule": { + "canonical_hash": "lowercase hex SHA-256 of canonical_json(canonical_payload)", + "signature_message": "UTF-8 bytes of binding_canonical_hash" + }, + "signature": { + "algorithm": "Ed25519", + "signature_field": "binding_signature", + "signature_encoding": "ed25519:", + "key_id_field": "binding_key_id", + "key_resolution": "Resolve a permit_binding_signing key from the supplied trust root at binding_issued_at or issued_at; the key id must match the signed payload binding_key_id." + }, + "verdicts": { + "supported": "canonical payload is present or reconstructable as issuance-time evidence, binding_canonical_hash matches, signature verifies under a trusted permit-binding key, and decision matches the claim request.", + "disproved": "canonical bytes or decision disagree with binding_canonical_hash, or a supplied signature fails under the resolved trusted key.", + "insufficient_evidence": "canonical payload fields, binding_canonical_hash, binding_signature, signing time, or trust-root evidence are missing.", + "unverifiable_scope": "binding version, canonicalization profile, or trust-root profile is unsupported." + } + } +} diff --git a/semantics/permit/dispatch_absence_after_revocation_v1.json b/semantics/permit/dispatch_absence_after_revocation_v1.json new file mode 100644 index 0000000..3cc55cb --- /dev/null +++ b/semantics/permit/dispatch_absence_after_revocation_v1.json @@ -0,0 +1,61 @@ +{ + "id": "keel.permit.dispatch_absence_after_revocation.v1", + "version": "v1", + "kind": "permit_dispatch_absence_after_revocation_recipe", + "status": "released", + "body": { + "claim": "permit.dispatch_absence_after_revocation.v1", + "artifact_type": "permit.dispatch_absence_after_revocation", + "artifact_version": "permit.dispatch_absence_after_revocation.v1", + "canonicalization_profile": "keel.canonical_json.payload.v1", + "doctrine": "scope-faithful absence adjudication", + "trust_model": "The verifier's trust model is falsifiability-oriented, not omniscience-oriented: an auditor with chain access can falsify a dishonest sidecar by independently re-walking the chain and recomputing the matching set against the declared predicate.", + "preconditions": [ + "permit.revoked.v1 evidence is supported for the same permit_id and project_id.", + "checkpoint.scope_state.v1 evidence is supported for the declared chain_scope and checkpoint boundary.", + "export.scope_faithfulness.v1 evidence is supported for the declared absence segment." + ], + "primary_predicate": { + "grammar_version": "keel.scope_predicate.v1", + "operator": "and", + "required_equals": { + "project_id": "", + "permit_id": "", + "event_type": "dispatch.egress_bound" + }, + "required_ranges": { + "occurred_at": { + "gte": "", + "lt": "" + } + }, + "range_rule": "Both occurred_at bounds are required. Open-ended ranges are not valid in v1.", + "grammar_rule": "v1 uses AND-of-equality plus bounded ranges only. IN, OR, and multi-event-type segments are out of grammar." + }, + "event_scope": { + "primary_disproof_event_type": "dispatch.egress_bound", + "corroborating_event_types": [ + "execution.completed", + "provider.response.received", + "client.response.delivered" + ], + "corroborating_rule": "Downstream completion events can corroborate timeline analysis but are not primary disproof because post-revocation completion can be legitimate for a pre-revocation dispatch." + }, + "sidecar_commitment": { + "commitment_profile": "keel.scope_state.merkle.v1", + "matching_count_rule": "supported requires the signed commitment for the declared predicate to have matching_count equal to zero and no disclosure records satisfying the predicate.", + "bridge_record_rule": "If a bridge, proof, or continuity record satisfies the declared predicate, the verifier returns disproved or EXPORT_SCOPE_BRIDGE_RECORD_MATCHES_PREDICATE; it does not return unverifiable_scope." + }, + "reserved_future_namespace": { + "name": "non_membership_profile", + "status": "reserved", + "rule": "Reserved for future SMT, NMT, or accumulator-backed native non-membership semantics. It is not implemented by v1 and MUST NOT be evaluated as current Step 4 behavior." + }, + "verdicts": { + "supported": "scope is valid, evidence dependencies are supported, the declared predicate is in grammar, and matching_count is zero.", + "disproved": "a matching post-revocation dispatch.egress_bound record exists in the declared scope, a predicate-satisfying bridge/proof record is present, or the signed commitment contradicts disclosed evidence.", + "insufficient_evidence": "required revocation, checkpoint, sidecar, export, trust-root, or pinned-semantic evidence is missing.", + "unverifiable_scope": "scope predicate, grammar version, range shape, or commitment profile is unsupported." + } + } +} diff --git a/semantics/permit/revoked_event_v1.json b/semantics/permit/revoked_event_v1.json new file mode 100644 index 0000000..9a6955b --- /dev/null +++ b/semantics/permit/revoked_event_v1.json @@ -0,0 +1,46 @@ +{ + "id": "keel.permit.revoked_event.v1", + "version": "v1", + "kind": "permit_revoked_event_signature_recipe", + "status": "released", + "body": { + "claim": "permit.revoked.v1", + "artifact_type": "permit.revoked", + "artifact_version": "permit.revoked.v1", + "canonicalization_profile": "keel.canonical_json.payload.v1", + "issuer_key_purpose": "permit_binding_signing", + "cutover_rule": "This claim applies only to new post-cutover permit.revoked events. Pre-cutover revoked status is a legacy revocation status snapshot and is not revocation evidence for this claim.", + "canonical_payload": { + "required_fields": [ + "permit_id", + "project_id", + "actor_id", + "actor_kind", + "reason_code", + "revoked_at", + "effective_at" + ], + "signature_exclusion": "The signature field is excluded before canonicalization.", + "identity_binding": "permit_id and project_id are both signed to prevent cross-project replay.", + "actor_rule": "actor_id is an opaque UUID. actor_kind is one of user, service_account, system, or api_key. The payload carries no email, name, or display field.", + "reason_rule": "reason_code is a taxonomy code, not free text.", + "temporal_rule": "revoked_at and effective_at are RFC 3339 timestamps. In v1, effective_at MUST equal revoked_at." + }, + "hash_rule": { + "canonical_hash": "lowercase hex SHA-256 of canonical_json(payload_without_signature)", + "signature_message": "UTF-8 bytes of canonical_hash" + }, + "signature": { + "algorithm": "Ed25519", + "signature_field": "signature", + "signature_encoding": "base64", + "key_resolution": "Verify against a permit_binding_signing key from the supplied trust root active at revoked_at. If multiple active keys are supplied and no key id is carried by the event payload, any active trusted key may support the signature." + }, + "verdicts": { + "supported": "strict event payload is present, effective_at equals revoked_at, permit_id and project_id match the requested scope, and signature verifies under a trusted permit-binding key.", + "disproved": "signature verification fails, project_id does not match the requested scope, effective_at differs from revoked_at, or actor identity violates the opaque UUID rule.", + "insufficient_evidence": "required payload, signature, trust-root evidence, or referenced permit/project identity is missing.", + "unverifiable_scope": "canonicalization profile, trust-root profile, or actor_kind taxonomy is unsupported." + } + } +} diff --git a/spec/dispatch-absence-after-revocation-v1.md b/spec/dispatch-absence-after-revocation-v1.md new file mode 100644 index 0000000..38fe99d --- /dev/null +++ b/spec/dispatch-absence-after-revocation-v1.md @@ -0,0 +1,192 @@ +# Dispatch Absence After Revocation v1 + +This document specifies the +`permit.dispatch_absence_after_revocation.v1` verifier claim. + +The claim provides scope-faithful absence adjudication for post-revocation +dispatch initiation. It adjudicates the absence of `dispatch.egress_bound` +events within a declared, signed, checkpoint-bounded scope. It does not claim +omniscient knowledge of all possible history. + +--- + +## 1. Conformance Keywords + +MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, +MAY, and OPTIONAL are interpreted per [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). + +## 2. Claim Scope + +`permit.dispatch_absence_after_revocation.v1` depends on: + +- `permit.revoked.v1` for a supported signed revocation event. +- `checkpoint.scope_state.v1` for a supported signed sidecar bound to the + checkpoint and chain scope. +- `export.scope_faithfulness.v1` for a supported signed export segment whose + declared scope matches this claim. + +The primary predicate covers only dispatch initiation: + +```json +{ + "version": "keel.scope_predicate.v1", + "operator": "and", + "equals": { + "project_id": "", + "permit_id": "", + "event_type": "dispatch.egress_bound" + }, + "ranges": { + "occurred_at": { + "gte": "", + "lt": "" + } + } +} +``` + +Both `occurred_at` bounds are REQUIRED. Open-ended ranges are not valid for this +claim. + +The lower bound is `occurred_at >= revocation.effective_at`. The upper bound is +`occurred_at < checkpoint_boundary`. In v1, `revocation.effective_at` equals +`revocation.revoked_at` under +[`permit-revoked-event-v1.md`](permit-revoked-event-v1.md). + +## 3. Predicate Grammar + +The predicate MUST use `keel.scope_predicate.v1`, defined in +[`scope-predicate-v1.md`](scope-predicate-v1.md). v1 is AND-of-equality plus +bounded ranges only. `IN`, OR, NOT, wildcard, regex, script, CEL, Rego, and +multi-event-type predicates are out of grammar. + +Step 4 uses a single event type segment: +`event_type == "dispatch.egress_bound"`. + +A predicate that requires a grammar extension returns `unverifiable_scope` with +`EXPORT_SCOPE_PREDICATE_OUT_OF_GRAMMAR` or +`EXPORT_SCOPE_PREDICATE_UNSUPPORTED`. Predicate syntax that claims v1 but +violates the v1 shape returns `disproved` with +`EXPORT_SCOPE_PREDICATE_MALFORMED`. + +## 4. Primary and Corroborating Events + +The only primary disproof event type is `dispatch.egress_bound`. + +The following event types are corroborating evidence, not primary disproof: + +- `execution.completed` +- `provider.response.received` +- `client.response.delivered` + +Post-revocation completion can be legitimate for work that was dispatched +before revocation. A verifier MUST NOT disprove this claim solely because a +downstream completion event occurs after `revoked_at`. + +## 5. Scope-Faithful Absence Adjudication + +Scope-faithful absence adjudication means the verifier checks the declared +predicate against the supplied signed export segment and its signed +checkpoint-scope sidecar. For this claim, `supported` requires: + +- The revocation event is supported for the same `permit_id` and `project_id`. +- The declared predicate exactly covers the post-revocation + `dispatch.egress_bound` scope described in §2. +- The sidecar commitment for that predicate has `matching_count == 0`. +- No disclosure, bridge, proof, or continuity record supplied in the pack + satisfies the declared predicate. +- The checkpoint boundary is the declared upper bound. + +If a bridge, proof, or continuity record satisfies the predicate, the verifier +returns `disproved` or emits +`EXPORT_SCOPE_BRIDGE_RECORD_MATCHES_PREDICATE`. This is not +`unverifiable_scope`: the predicate can be evaluated, and the supplied evidence +contradicts the claimed empty matching set. + +## 6. Trust Model + +The verifier's trust model is falsifiability-oriented, not +omniscience-oriented. Keel's verifier does not claim omniscient proof over all +possible history; it claims independently auditable scoped evidence whose +dishonesty is detectable under declared assumptions. + +An auditor with chain access can falsify a dishonest sidecar by independently +re-walking the chain and recomputing the matching set against the declared +predicate. + +## 7. Standards Context + +Keel's v1 scope-state commitment uses RFC 9162-style Merkle tree construction +for membership roots. [RFC 9162](https://www.rfc-editor.org/rfc/rfc9162.html) +defines Certificate Transparency v2 and specifies Merkle inclusion and +consistency proof machinery for append-only transparency logs. + +The COSE Merkle Tree Proofs draft +([draft-ietf-cose-merkle-tree-proofs](https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/)) +registers COSE receipt structures for verifiable data structure proofs, +including RFC 9162 inclusion and consistency receipts. This v1 claim does not +depend on a COSE proof type for native non-membership. + +The SCITT architecture draft +([draft-ietf-scitt-architecture](https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/)) +describes receipts and Verifiable Data Structures as implementation-specific +proof semantics for transparency services. Keel's v1 claim therefore states +its own scoped predicate, checkpoint, sidecar, and trust assumptions rather +than importing stronger semantics from SCITT or COSE. + +## 8. Reserved Future Namespace + +`non_membership_profile` is a reserved semantic-registry namespace for future +SMT, NMT, or accumulator-backed native non-membership semantics. It is not +implemented in v1, and v1 verifiers MUST NOT evaluate it as current Step 4 +behavior. + +The reservation follows the same name-reservation discipline as +[`permit-v1.md`](permit-v1.md) §11 reserves `signature` and +`counter_signature`. + +## 9. Correct Framing + +Correct: "The verifier performs scope-faithful absence adjudication for +post-revocation `dispatch.egress_bound` events within the declared signed scope +and checkpoint boundary." + +Correct: "A post-revocation `execution.completed` event may corroborate a +timeline, but it does not by itself disprove the claim." + +Incorrect: "The verifier proves that no execution occurred after revocation." + +Incorrect: "A completion event after revocation always disproves the claim." + +## 10. Claim Verdicts + +`supported`: the scope is valid, all dependency claims are supported, the +predicate is in grammar, the matching count is zero, and no supplied record +satisfies the predicate. + +`disproved`: a matching post-revocation `dispatch.egress_bound` record exists +in the declared scope, a bridge/proof/continuity record satisfies the +predicate, or the signed sidecar commitment contradicts disclosed evidence. + +`insufficient_evidence`: required revocation, checkpoint, sidecar, export, +trust-root, or pinned-semantic evidence is missing. + +`unverifiable_scope`: the scope predicate, grammar version, range shape, or +commitment profile is unsupported. + +## 11. Failure Codes + +Applicable standard failure codes are defined in +[`failure-codes.md`](failure-codes.md): + +- `EXPORT_SCOPE_PREDICATE_OUT_OF_GRAMMAR` +- `EXPORT_SCOPE_PREDICATE_UNSUPPORTED` +- `EXPORT_SCOPE_PREDICATE_MALFORMED` +- `EXPORT_SCOPE_BRIDGE_RECORD_MATCHES_PREDICATE` +- `EXPORT_SCOPE_CARDINALITY_MISMATCH` +- `EXPORT_SCOPE_COMMITMENT_MISSING` +- `CHECKPOINT_SCOPE_STATE_MISSING` +- `CHECKPOINT_SCOPE_STATE_COMMITMENT_PROFILE_UNKNOWN` + +The pinned semantic artifact is +[`../semantics/permit/dispatch_absence_after_revocation_v1.json`](../semantics/permit/dispatch_absence_after_revocation_v1.json). diff --git a/spec/failure-codes.md b/spec/failure-codes.md index ab7f7ee..499e907 100644 --- a/spec/failure-codes.md +++ b/spec/failure-codes.md @@ -37,6 +37,15 @@ MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY | `WALK_PERMIT_CHAIN_ENVELOPE_INCONSISTENT` | permit chain | A Permit's top-level `expires_at` and `authority_envelope.expires_at` disagree. | | `WALK_PERMIT_CHAIN_RECEIPT_MISSING` | permit chain | A claim required an execution receipt and none is present. | | `WALK_PERMIT_CHAIN_ANCESTOR_MISSING` | permit chain | Lineage does not reach a root within the supplied pack and no completeness checkpoint covers the ancestor scope. | +| `PERMIT_DECISION_SIGNATURE_INVALID` | permit decision | A permit decision binding signature does not verify under the resolved permit-binding key. | +| `PERMIT_DECISION_CANONICAL_PAYLOAD_MISMATCH` | permit decision | The issuance-time canonical payload does not hash to `binding_canonical_hash`, or the payload decision does not match the adjudicated decision. | +| `PERMIT_DECISION_KEY_NOT_TRUSTED` | permit decision | No trusted permit-binding key resolves for the decision binding at signing time. | +| `PERMIT_REVOKED_SIGNATURE_INVALID` | permit revoked | A signed `permit.revoked` event signature does not verify under a trusted permit-binding key. | +| `PERMIT_REVOKED_PROJECT_ID_MISMATCH` | permit revoked | A signed `permit.revoked` event is presented for a different `project_id` than the claim scope. | +| `PERMIT_REVOKED_EFFECTIVE_AT_NOT_EQUAL_REVOKED_AT` | permit revoked | A v1 `permit.revoked` event has `effective_at` different from `revoked_at`. | +| `PERMIT_REVOKED_ACTOR_PII_DETECTED` | permit revoked | A revocation actor value is not an opaque UUID or actor identity fields carry PII-shaped data. | +| `EXPORT_SCOPE_PREDICATE_OUT_OF_GRAMMAR` | export scope | A declared scope predicate uses an operator or range shape outside scope-predicate v1. | +| `EXPORT_SCOPE_BRIDGE_RECORD_MATCHES_PREDICATE` | export scope | A bridge, proof, or continuity record satisfies the declared predicate. | A verifier MUST emit exactly one of these codes per detected violation. Implementations MAY emit multiple codes when a single artifact contains multiple independent violations. @@ -133,13 +142,36 @@ A verifier MUST emit exactly one of these codes per detected violation. Implemen **Verifier action**: report the relevant `permit_id`, parent/child boundary, envelope version, or missing comparator/receipt requirement. The verifier MUST NOT silently downgrade a semantic Permit Chain failure into a generic chain-walk failure when the cryptographic chain itself is intact. -## 13. Scope-State and Scope-Faithfulness Codes +## 13. Permit Decision and Revocation Codes Every code in this section maps to the existing v0 verdict enum: `supported`, `disproved`, `insufficient_evidence`, or `unverifiable_scope`. No new verdict values are introduced. -### 13.1 Sidecar Codes +### 13.1 Permit Decision Codes + +| Code | Verdict | Trigger | +|---|---|---| +| `PERMIT_DECISION_SIGNATURE_INVALID` | `disproved` | `binding_signature` is present but does not verify over `binding_canonical_hash` under the resolved permit-binding key. | +| `PERMIT_DECISION_CANONICAL_PAYLOAD_MISMATCH` | `disproved` | The supplied issuance-time canonical payload does not hash to `binding_canonical_hash`, the payload `decision` is not the adjudicated decision, or the pack attempts to substitute later mutable row state for issuance-time evidence. | +| `PERMIT_DECISION_KEY_NOT_TRUSTED` | `insufficient_evidence` | The pack does not supply a trusted permit-binding key active at `binding_issued_at` or the signing time declared for the decision evidence. | + +### 13.2 Permit Revoked Codes + +| Code | Verdict | Trigger | +|---|---|---| +| `PERMIT_REVOKED_SIGNATURE_INVALID` | `disproved` | The `permit.revoked` payload signature does not verify under a trusted permit-binding key active at `revoked_at`. | +| `PERMIT_REVOKED_PROJECT_ID_MISMATCH` | `disproved` | The signed `project_id` differs from the permit/project scope under adjudication. | +| `PERMIT_REVOKED_EFFECTIVE_AT_NOT_EQUAL_REVOKED_AT` | `disproved` | The v1 event has `effective_at` different from `revoked_at`. Scheduled revocation is not a v1 behavior. | +| `PERMIT_REVOKED_ACTOR_PII_DETECTED` | `disproved` | The actor identity is not an opaque UUID, or the event contains actor email, name, display label, or other PII-shaped identity material. Strict v1 payloads reject these fields; this code is a defensive failure for lax or translated evidence inputs. | + +## 14. Scope-State and Scope-Faithfulness Codes + +Every code in this section maps to the existing v0 verdict enum: `supported`, +`disproved`, `insufficient_evidence`, or `unverifiable_scope`. No new verdict +values are introduced. + +### 14.1 Sidecar Codes | Code | Verdict | Trigger | |---|---|---| @@ -158,13 +190,14 @@ values are introduced. | `CHECKPOINT_SCOPE_STATE_PREDICATE_HASH_MISMATCH` | `disproved` | `predicate_value_hash` does not match canonical predicate value. | | `CHECKPOINT_SCOPE_STATE_COMMITMENT_PREDICATE_DUPLICATE` | `disproved` | Multiple `scope_commitments[]` entries share the same `predicate_value_hash`. | -### 13.2 Export Codes +### 14.2 Export Codes | Code | Verdict | Trigger | |---|---|---| | `EXPORT_SCOPE_DECLARATION_MISSING` | `insufficient_evidence` | Signed export payload has no `scope_faithfulness` block or segment. | | `EXPORT_SCOPE_DECLARATION_SCHEMA_INVALID` | `disproved` | Present declaration violates strict v1 schema. | | `EXPORT_SCOPE_PREDICATE_UNSUPPORTED` | `unverifiable_scope` | Predicate grammar or predicate kind is outside v1. | +| `EXPORT_SCOPE_PREDICATE_OUT_OF_GRAMMAR` | `unverifiable_scope` | Predicate uses an operator or range shape outside scope-predicate v1, including `IN`, OR, unsupported range bounds, or an open-ended range. | | `EXPORT_SCOPE_PREDICATE_MALFORMED` | `disproved` | Predicate claims v1 but is syntactically invalid. | | `EXPORT_SCOPE_PREDICATE_VIOLATED` | `disproved` | A disclosure record does not satisfy the structured predicate. | | `EXPORT_PRESENTATION_POLICY_UNSUPPORTED` | `unverifiable_scope` | `presentation_policy.version` or kind is unsupported. | @@ -184,16 +217,17 @@ values are introduced. | `EXPORT_CHAIN_PROOF_MISSING` | `insufficient_evidence` | Required chain entries or proof bridge records are absent. | | `EXPORT_CHAIN_PROOF_DISCONTINUITY` | `disproved` | Supplied chain evidence fails local continuity. | | `EXPORT_PROOF_BRIDGE_MISCLASSIFIED` | `disproved` | A bridge record is counted as in-scope or a disclosure record is marked bridge-only. | +| `EXPORT_SCOPE_BRIDGE_RECORD_MATCHES_PREDICATE` | `disproved` | A bridge, proof, or continuity record satisfies the declared predicate; the verifier can evaluate the predicate and the evidence contradicts the claimed empty matching set. | | `EXPORT_SCOPE_COMMITMENT_MISSING` | `insufficient_evidence` | Sidecar has no commitment for declared predicate hash. | | `EXPORT_SCOPE_CARDINALITY_MISMATCH` | `disproved` | Signed `matching_count` differs from disclosure record count. | | `EXPORT_SCOPE_MEMBERSHIP_ROOT_MISMATCH` | `disproved` | Recomputed Merkle root differs from signed sidecar root. | | `EXPORT_SCOPE_RANGE_MISMATCH` | `disproved` | Disclosure min/max sequence differs from signed matching range. | -## 14. Code stability +## 15. Code stability The codes in this document are **stable identifiers**. New codes MAY be added by future spec revisions; existing codes MUST NOT be renamed, repurposed, or removed. A code's literal string is part of the public contract for downstream tooling (alerting, dashboards, automated incident response). -## 15. Verifier output format +## 16. Verifier output format This spec does not constrain how a verifier surfaces failure codes — JSON, structured logs, exit codes, human-readable text are all permitted. The MUST is that the literal code strings appear unchanged. diff --git a/spec/permit-revoked-event-v1.md b/spec/permit-revoked-event-v1.md new file mode 100644 index 0000000..59544f8 --- /dev/null +++ b/spec/permit-revoked-event-v1.md @@ -0,0 +1,109 @@ +# Permit Revoked Event v1 + +This document specifies the signed `permit.revoked` event payload and the +`permit.revoked.v1` verifier claim. + +`permit.revoked.v1` is a post-cutover claim for newly emitted signed revocation +events. It does not retrofit historical mutable permit rows into revocation +events. + +--- + +## 1. Conformance Keywords + +MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, +MAY, and OPTIONAL are interpreted per [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). + +## 2. Event Shape + +The strict JSON Schema is +[`../schemas/permit-revoked-event.schema.json`](../schemas/permit-revoked-event.schema.json). + +Required fields: + +| Field | Type | Description | +|---|---|---| +| `permit_id` | UUID | Identifier of the revoked permit. | +| `project_id` | UUID | Identifier of the issuing project. This value is signed to prevent cross-project replay. | +| `actor_id` | UUID | Opaque revocation actor identifier. | +| `actor_kind` | enum | One of `user`, `service_account`, `system`, or `api_key`. | +| `reason_code` | string | Machine-readable taxonomy code. Free text is not part of the signed v1 payload. | +| `revoked_at` | RFC 3339 timestamp | Governance event time for the revocation. | +| `effective_at` | RFC 3339 timestamp | Effective revocation time. v1 requires this to equal `revoked_at`. | +| `signature` | base64 | Ed25519 signature over the canonical revocation payload hash. | + +The schema is closed. Unknown fields MUST be rejected. + +## 3. Canonical Payload + +The signed payload is the event object with `signature` removed. The payload is +serialized with `keel.canonical_json.payload.v1`: sorted keys, compact +separators, UTF-8, `ensure_ascii=false`, and no volatile-key stripping. + +The canonical hash is lowercase hex SHA-256 of those canonical JSON bytes. The +Ed25519 signature signs the UTF-8 bytes of that canonical hash, using a +permit-binding signing key. + +The signature field carries base64 for the raw 64-byte Ed25519 signature. + +## 4. Identity Binding + +Both `permit_id` and `project_id` are mandatory signed fields. A verifier MUST +compare both values against the claim scope. A matching permit ID with a +different project ID is `disproved` with +`PERMIT_REVOKED_PROJECT_ID_MISMATCH`. + +`actor_id` MUST be opaque. It MUST NOT carry an email address, display name, +natural-language label, external ticket, or customer-controlled identifier. A +verifier that detects PII-shaped actor identity material MUST return +`disproved` with `PERMIT_REVOKED_ACTOR_PII_DETECTED`. + +## 5. Effective Time + +v1 revocation is immediate. `effective_at` MUST equal `revoked_at`. + +Scheduled revocation is reserved for a future version. A v1 event with +different `effective_at` and `revoked_at` MUST return `disproved` with +`PERMIT_REVOKED_EFFECTIVE_AT_NOT_EQUAL_REVOKED_AT`. + +## 6. Legacy Status Snapshots + +Pre-cutover revoked permits remain runtime-only lifecycle status snapshots. +They are legacy revocation status snapshots, not revocation evidence for +`permit.revoked.v1`. + +A verifier MUST NOT infer a signed revocation event from a historical permit row +whose mutable `status` is `revoked`. If the signed event payload is missing, +the claim verdict is `insufficient_evidence`. + +## 7. Claim Verdicts + +`supported`: the strict event payload is present, `effective_at == revoked_at`, +the signed `permit_id` and `project_id` match the claim scope, and the Ed25519 +signature verifies under a trusted permit-binding key active at `revoked_at`. + +`disproved`: the signature fails, the signed project identity does not match, +`effective_at` differs from `revoked_at`, actor identity violates the opaque-ID +rule, or the canonical payload hash does not match the signed bytes. + +`insufficient_evidence`: required event payload, signature, trust-root evidence, +or permit/project identity evidence is missing. + +`unverifiable_scope`: the canonicalization profile, key manifest profile, or +actor taxonomy is unsupported. + +## 8. Failure Codes + +Applicable standard failure codes are defined in +[`failure-codes.md`](failure-codes.md): + +- `PERMIT_REVOKED_SIGNATURE_INVALID` +- `PERMIT_REVOKED_PROJECT_ID_MISMATCH` +- `PERMIT_REVOKED_EFFECTIVE_AT_NOT_EQUAL_REVOKED_AT` +- `PERMIT_REVOKED_ACTOR_PII_DETECTED` +- `UNKNOWN_SIGNING_KEY` +- `KEY_EXPIRED_AT_SIGNING_TIME` +- `SIGNING_KEY_REVOKED` + +The pinned semantic artifact is +[`../semantics/permit/revoked_event_v1.json`](../semantics/permit/revoked_event_v1.json). diff --git a/test-vectors/verifier_claims/v0/corpus.json b/test-vectors/verifier_claims/v0/corpus.json index b53fa87..108ef1a 100644 --- a/test-vectors/verifier_claims/v0/corpus.json +++ b/test-vectors/verifier_claims/v0/corpus.json @@ -5,7 +5,7 @@ "spec": "../../../spec/verifier-claims-v0.md", "version": "verifier-claims.v0" }, - "corpus_version": "verifier-claims-v0-golden-2026-05-20", + "corpus_version": "verifier-claims-v0-golden-2026-05-21-step4", "current_verifier_granularity": "Current verifiers expose whole-pack PASS/FAIL plus reason classes. Per-claim doctrine verdicts remain in each record for the P1-M structured verdict upgrade.", "records": [ { @@ -2141,6 +2141,198 @@ }, "payload_hash": "sha256:e1ba5c00b73824598f477e9343dc7088739a219ae995f99724ef44b226fae661", "title": "Chain-scope mismatch" + }, + { + "claims": [ + { + "expected_verdict": "supported", + "name": "export.integrity.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.decision.v1" + } + ], + "expected_current": { + "outcome": "PASS", + "reason_classes": [] + }, + "id": "permit-decision-allow-valid", + "kind": "export", + "pack": { + "export_file": "fixtures/permit-decision-allow-valid/pack/export.json", + "features": [ + "step4_permit_claims" + ], + "key_manifest": "trust_roots/step4-permit-claims-trust-root.json", + "manifest": "fixtures/permit-decision-allow-valid/pack/manifest.json" + }, + "title": "Valid signed permit allow decision" + }, + { + "claims": [ + { + "expected_verdict": "supported", + "name": "export.integrity.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.decision.v1" + } + ], + "expected_current": { + "outcome": "PASS", + "reason_classes": [] + }, + "id": "permit-decision-deny-valid", + "kind": "export", + "pack": { + "export_file": "fixtures/permit-decision-deny-valid/pack/export.json", + "features": [ + "step4_permit_claims" + ], + "key_manifest": "trust_roots/step4-permit-claims-trust-root.json", + "manifest": "fixtures/permit-decision-deny-valid/pack/manifest.json" + }, + "title": "Valid signed permit deny decision" + }, + { + "claims": [ + { + "expected_verdict": "supported", + "name": "export.integrity.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.decision.v1" + } + ], + "expected_current": { + "outcome": "PASS", + "reason_classes": [] + }, + "id": "permit-decision-challenge-valid", + "kind": "export", + "pack": { + "export_file": "fixtures/permit-decision-challenge-valid/pack/export.json", + "features": [ + "step4_permit_claims" + ], + "key_manifest": "trust_roots/step4-permit-claims-trust-root.json", + "manifest": "fixtures/permit-decision-challenge-valid/pack/manifest.json" + }, + "title": "Valid signed permit challenge decision" + }, + { + "claims": [ + { + "expected_verdict": "supported", + "name": "export.integrity.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.revoked.v1" + } + ], + "expected_current": { + "outcome": "PASS", + "reason_classes": [] + }, + "id": "permit-revoked-valid", + "kind": "export", + "pack": { + "export_file": "fixtures/permit-revoked-valid/pack/export.json", + "features": [ + "step4_permit_claims" + ], + "key_manifest": "trust_roots/step4-permit-claims-trust-root.json", + "manifest": "fixtures/permit-revoked-valid/pack/manifest.json" + }, + "title": "Valid signed permit revocation event" + }, + { + "claims": [ + { + "expected_verdict": "supported", + "name": "export.integrity.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.revoked.v1" + }, + { + "expected_verdict": "supported", + "name": "checkpoint.scope_state.v1" + }, + { + "expected_verdict": "supported", + "name": "export.scope_faithfulness.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.dispatch_absence_after_revocation.v1" + } + ], + "expected_current": { + "outcome": "PASS", + "reason_classes": [] + }, + "id": "dispatch-absence-after-revocation-valid-empty-scope", + "kind": "export", + "pack": { + "checkpoint_file": "fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/checkpoint.json", + "export_file": "fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/export.json", + "features": [ + "scope_faithfulness", + "step4_permit_claims" + ], + "key_manifest": "trust_roots/step4-permit-claims-trust-root.json", + "manifest": "fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/manifest.json", + "sidecar_file": "fixtures/dispatch-absence-after-revocation-valid-empty-scope/sidecars/checkpoint-scope-state-v1.json" + }, + "title": "Valid post-revocation dispatch absence with empty matching scope" + }, + { + "claims": [ + { + "expected_verdict": "supported", + "name": "export.integrity.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.revoked.v1" + }, + { + "expected_verdict": "supported", + "name": "checkpoint.scope_state.v1" + }, + { + "expected_verdict": "supported", + "name": "export.scope_faithfulness.v1" + }, + { + "expected_verdict": "supported", + "name": "permit.dispatch_absence_after_revocation.v1" + } + ], + "expected_current": { + "outcome": "PASS", + "reason_classes": [] + }, + "id": "dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch", + "kind": "export", + "pack": { + "checkpoint_file": "fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/checkpoint.json", + "export_file": "fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/export.json", + "features": [ + "scope_faithfulness", + "step4_permit_claims" + ], + "key_manifest": "trust_roots/step4-permit-claims-trust-root.json", + "manifest": "fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/manifest.json", + "sidecar_file": "fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/sidecars/checkpoint-scope-state-v1.json" + }, + "title": "Valid post-revocation dispatch absence with pre-revocation dispatch evidence" } ], "soundness_rule": "Expected doctrine verdicts are derived from claim_registry/v0.json and spec/verifier-claims-v0.md, never from current verifier output." diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/README.md b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/README.md new file mode 100644 index 0000000..df96902 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/README.md @@ -0,0 +1,11 @@ +# dispatch-absence-after-revocation-valid-empty-scope + +Valid post-revocation dispatch absence with empty matching scope. + +## What It Tests + +The pack contains supported revocation evidence and a scope-faithful absence adjudication segment whose post-revocation `dispatch.egress_bound` matching count is zero. + +## Expected Verdict + +`permit.dispatch_absence_after_revocation.v1` is expected to return `supported`. diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/checkpoint.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/checkpoint.json new file mode 100644 index 0000000..dd6ef89 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/checkpoint.json @@ -0,0 +1,49 @@ +{ + "chain_heads": { + "project:00000000-0000-0000-0000-000000000041": { + "last_record_hash": "da137414bf851df9e30e1f86e95c13b387a36ed6152816452de982c743050b7d", + "sequence_number": 1 + } + }, + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "claim_set": { + "claims": [ + { + "name": "checkpoint.composite_hash.v1", + "required": true + }, + { + "name": "checkpoint.signature.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "composite_hash": "sha256:3fd43b6a0911bd5da20e720090f87f8ef106dee0ba2383ea2ad2d51488f83ba4", + "computed_at": "2026-05-21T10:10:00.000000Z", + "keel_version": "step4-permit-claims-fixtures", + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "public_key": "ed25519:oJql9HpnWYAv+VX43C0qFKXJnSO+l/hkEn/5ODRVpPA=", + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:68aafa26d6f1c8cf5ba83c7596209888d8e529d81f1a2c58f31e2fc41fc136de", + "id": "keel.checkpoint.composite_hash.v1", + "path": "semantics/checkpoint/composite_hash_v1.json" + }, + { + "hash": "sha256:af16c66e8a0b295cd2e5e436169bf0e3d628c1fc4901b6eba6596e86e3ad256b", + "id": "keel.checkpoint.signature.v1", + "path": "semantics/checkpoint/signature_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:RysiE3TOSHVFkxliH9NJlo4XdZc3J+xaI/MADBiFuYrQatXmCWQJ2aKBS7sIGfypK8FJDRa9sqngG5T5mF+UDw==" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/export.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/export.json new file mode 100644 index 0000000..ca93b6f --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/export.json @@ -0,0 +1,125 @@ +{ + "fixture_id": "dispatch-absence-after-revocation-valid-empty-scope", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "revocation_event": { + "canonical_hash": "988d47a4ae347d0b4f49866fc2414acec0139c4be103f3491ae53fd6f2cfe2b2", + "event": { + "actor_id": "30000000-0000-4000-8000-000000000001", + "actor_kind": "user", + "effective_at": "2026-05-21T10:05:00.000000Z", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "reason_code": "operator.requested", + "revoked_at": "2026-05-21T10:05:00.000000Z", + "signature": "ULWxnis0lPSVXh3Hw2di7GUwu/KPflmoKX2PsTkXua2IF53KPduIdYwXy7SkPyERq/FhiEecHBU0qDV0DBVHBw==" + } + }, + "schema": "keel.step4.permit_claim_fixture/v1", + "scope_faithfulness": { + "segments": [ + { + "canonical_filters": { + "canonicalization_profile": "keel.canonical_json.payload.v1", + "filters_hash": "sha256:ce17beeb939a1868b176abcce5003a0dc3f6344d4cdaf3fd47a98eb3a4d067ad", + "raw_filters": { + "equals": { + "event_type": "dispatch.egress_bound", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "operator": "and", + "ranges": { + "occurred_at": { + "gte": "2026-05-21T10:05:00.000000Z", + "lt": "2026-05-21T10:10:00.000000Z" + } + }, + "version": "keel.scope_predicate.v1" + } + }, + "chain_evidence": { + "disclosure_records": [], + "proof_bridge_records": [ + { + "chain_format_version": "v1", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "created_at": "2026-05-21T10:05:00.000000Z", + "event_id": "evt_step4_permit_revoked", + "event_type": "permit.revoked", + "occurred_at": "2026-05-21T10:05:00.000000Z", + "outcome": "success", + "payload_json": { + "effective_at": "2026-05-21T10:05:00.000000Z", + "event_type": "permit.revoked", + "occurred_at": "2026-05-21T10:05:00.000000Z", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "reason_code": "operator.requested", + "revoked_at": "2026-05-21T10:05:00.000000Z" + }, + "permit_id": "20000000-0000-4000-8000-000000000001", + "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "project_id": "00000000-0000-0000-0000-000000000041", + "record_hash": "da137414bf851df9e30e1f86e95c13b387a36ed6152816452de982c743050b7d", + "resource_id": "20000000-0000-4000-8000-000000000001", + "resource_type": "permit", + "sequence_number": 1, + "severity": "info" + } + ] + }, + "declared_end": { + "boundary_policy": "explicit_checkpoint", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "checkpoint_boundary": "2026-05-21T10:10:00.000000Z", + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "last_record_hash": "da137414bf851df9e30e1f86e95c13b387a36ed6152816452de982c743050b7d", + "sequence_number": 1 + }, + "declared_scope": { + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "population_label": "Post-revocation dispatch.egress_bound events for one permit", + "predicate": { + "equals": { + "event_type": "dispatch.egress_bound", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "operator": "and", + "ranges": { + "occurred_at": { + "gte": "2026-05-21T10:05:00.000000Z", + "lt": "2026-05-21T10:10:00.000000Z" + } + }, + "version": "keel.scope_predicate.v1" + }, + "presentation_policy": { + "policy_kind": "none", + "policy_parameters": {}, + "version": "keel.presentation_policy.v1" + }, + "scope_kind": "declared_sample", + "version": "keel.scope_declaration.v1" + }, + "declared_start": { + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "genesis_prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "kind": "genesis", + "sequence_number": 1 + }, + "scope_state_reference": { + "artifact_hash": "sha256:de0471fad67c81de5d25ff14aa3ffa49332f889bcbb1589e4042205db2cb736b", + "artifact_type": "checkpoint_scope_state", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "scope_state_id": "keel.scope_state.v1:bd32ee1f1b8934ffc3688db3454a7049ebd808e866d2338d9fce48943abc7a92:40000000-0000-4000-8000-000000000001", + "storage_uri": "scope-state/v1/bd32ee1f1b8934ffc3688db3454a7049ebd808e866d2338d9fce48943abc7a92/40000000-0000-4000-8000-000000000001/checkpoint-scope-state-v1.json" + }, + "segment_id": "dispatch-absence-after-revocation-valid-empty-scope" + } + ], + "version": "keel.export_scope_faithfulness.v1" + } +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/manifest.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/manifest.json new file mode 100644 index 0000000..0611ee7 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/pack/manifest.json @@ -0,0 +1,94 @@ +{ + "claim_set": { + "claims": [ + { + "name": "export.integrity.v1", + "required": true + }, + { + "name": "permit.revoked.v1", + "required": true + }, + { + "name": "checkpoint.scope_state.v1", + "required": true + }, + { + "name": "export.scope_faithfulness.v1", + "required": true + }, + { + "name": "permit.dispatch_absence_after_revocation.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "compressed": false, + "content_hash": "sha256:2a85ba427e0178bcd3c4537ca6f3cfd0d001c843ca058d52cb378b697f9aa94e", + "export_id": "dispatch-absence-after-revocation-valid-empty-scope", + "export_type": "audit_export", + "format": "json", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "project_id": "00000000-0000-0000-0000-000000000041", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "record_count": 0, + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:d1d67dca7eb9a662d26463c3dec841f47f8791df2fafb21e911dd26a83dabb76", + "id": "keel.export_manifest.integrity.v1", + "path": "semantics/export_manifest/integrity_v1.json" + }, + { + "hash": "sha256:5b7416b11a4a94a2f9f876b2337e118267ab5eb928165cc15f01abaff8639229", + "id": "keel.permit.revoked_event.v1", + "path": "semantics/permit/revoked_event_v1.json" + }, + { + "hash": "sha256:68aafa26d6f1c8cf5ba83c7596209888d8e529d81f1a2c58f31e2fc41fc136de", + "id": "keel.checkpoint.composite_hash.v1", + "path": "semantics/checkpoint/composite_hash_v1.json" + }, + { + "hash": "sha256:af16c66e8a0b295cd2e5e436169bf0e3d628c1fc4901b6eba6596e86e3ad256b", + "id": "keel.checkpoint.signature.v1", + "path": "semantics/checkpoint/signature_v1.json" + }, + { + "hash": "sha256:a3213706c9e9531a74cd2355f2f05e537c7a70604cb869b7b76c65cba4a2b707", + "id": "keel.governance_chain.record_hash.v1", + "path": "semantics/governance_chain/record_hash_v1.json" + }, + { + "hash": "sha256:7fc40790b6d8552b8bff63bbfa69cdd53f744a98be97c217e832ea3299e7b528", + "id": "keel.scope_state.merkle.v1", + "path": "semantics/scope_state/merkle_v1.json" + }, + { + "hash": "sha256:f54ac8a8a0c9fb26ee5870e9aded865376af9dde4899026542e97df5d9f454fd", + "id": "keel.scope_state.sidecar_format.v1", + "path": "semantics/scope_state/sidecar_format_v1.json" + }, + { + "hash": "sha256:478150048a5135ebba4550806a814b27ced491a1198c41ad5a40390045a1435b", + "id": "keel.export.scope_faithfulness.v1", + "path": "semantics/export/scope_faithfulness_v1.json" + }, + { + "hash": "sha256:529f17bf4de5ab0ae4a85b89dd66894ddc65923825defae41d5e8af57d0cc0c4", + "id": "keel.permit.dispatch_absence_after_revocation.v1", + "path": "semantics/permit/dispatch_absence_after_revocation_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:JBfDVAcEck97vy2JFbX90GcZGSnWNlyLTRCFIGSh4kLNZUmM4PBvZ6EWKhR8rTiSb5W7IQJGH3nb831QFxylBg==", + "signed_at": "2026-05-21T10:00:00.000000Z" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/sidecars/checkpoint-scope-state-v1.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/sidecars/checkpoint-scope-state-v1.json new file mode 100644 index 0000000..f018db3 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-empty-scope/sidecars/checkpoint-scope-state-v1.json @@ -0,0 +1,68 @@ +{ + "artifact_type": "checkpoint_scope_state", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "commitment_profile": "keel.scope_state.merkle.v1", + "predicate_basis": { + "canonicalization_profile": "keel.canonical_json.payload.v1", + "reserved_namespaces": [ + "keel.scope_predicate.reserved.v1", + "non_membership_profile" + ], + "supported_predicate_kinds": [ + "project_id", + "permit_id", + "request_id", + "event_type", + "category", + "severity", + "decision_type", + "policy_id", + "provider", + "sequence_number", + "created_at", + "occurred_at", + "section", + "export_type" + ] + }, + "predicate_grammar_version": "keel.scope_predicate.v1", + "scope_commitments": [ + { + "first_matching_sequence": null, + "last_matching_sequence": null, + "matching_count": 0, + "membership_root_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "predicate_value": { + "equals": { + "event_type": "dispatch.egress_bound", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "operator": "and", + "ranges": { + "occurred_at": { + "gte": "2026-05-21T10:05:00.000000Z", + "lt": "2026-05-21T10:10:00.000000Z" + } + }, + "version": "keel.scope_predicate.v1" + }, + "predicate_value_hash": "sha256:ce17beeb939a1868b176abcce5003a0dc3f6344d4cdaf3fd47a98eb3a4d067ad" + } + ], + "scope_state_id": "keel.scope_state.v1:bd32ee1f1b8934ffc3688db3454a7049ebd808e866d2338d9fce48943abc7a92:40000000-0000-4000-8000-000000000001", + "signature": { + "algorithm": "Ed25519", + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "signature": "ed25519:MS9/fceZLVmv+VpH4JgpuqXcVMwUgUH3hvmY2co5n/FQ7a8FD3CPgu0uVDG3RLa28shlzi57rqE55XqjEs2DDw==" + }, + "signed_at": "2026-05-21T10:00:00.000000Z", + "tree_size": 1, + "trust_root_reference": { + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "manifest_version": "keel.public_key_manifest.v1", + "purpose": "scope_state" + }, + "version": "checkpoint_scope_state.v1" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/README.md b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/README.md new file mode 100644 index 0000000..18b6165 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/README.md @@ -0,0 +1,11 @@ +# dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch + +Valid post-revocation dispatch absence with pre-revocation dispatch evidence. + +## What It Tests + +The pack contains supported revocation evidence and a scope-faithful absence adjudication segment whose post-revocation `dispatch.egress_bound` matching count is zero. A pre-revocation `dispatch.egress_bound` record is supplied as bridge evidence and does not match the bounded `occurred_at` range. + +## Expected Verdict + +`permit.dispatch_absence_after_revocation.v1` is expected to return `supported`. diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/checkpoint.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/checkpoint.json new file mode 100644 index 0000000..f114fd5 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/checkpoint.json @@ -0,0 +1,49 @@ +{ + "chain_heads": { + "project:00000000-0000-0000-0000-000000000041": { + "last_record_hash": "051485e92efe5e467f34fcfa91c59cc84752624d4c05e224956a1cc3558afa35", + "sequence_number": 2 + } + }, + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "claim_set": { + "claims": [ + { + "name": "checkpoint.composite_hash.v1", + "required": true + }, + { + "name": "checkpoint.signature.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "composite_hash": "sha256:e1d895aa7dacd6a858819b05aaab92fd4733993d529d43d0020c7f521df0bdd7", + "computed_at": "2026-05-21T10:10:00.000000Z", + "keel_version": "step4-permit-claims-fixtures", + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "public_key": "ed25519:oJql9HpnWYAv+VX43C0qFKXJnSO+l/hkEn/5ODRVpPA=", + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:68aafa26d6f1c8cf5ba83c7596209888d8e529d81f1a2c58f31e2fc41fc136de", + "id": "keel.checkpoint.composite_hash.v1", + "path": "semantics/checkpoint/composite_hash_v1.json" + }, + { + "hash": "sha256:af16c66e8a0b295cd2e5e436169bf0e3d628c1fc4901b6eba6596e86e3ad256b", + "id": "keel.checkpoint.signature.v1", + "path": "semantics/checkpoint/signature_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:bEVAzxxHmPpgpkvjH/yc3JBrK2lJqE+lqn9ykeA3uxOmRz7xi5YWEf3VerjDlb08W8V/C6aYXid2MpvhhsIwCQ==" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/export.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/export.json new file mode 100644 index 0000000..ce83d2d --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/export.json @@ -0,0 +1,149 @@ +{ + "fixture_id": "dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "revocation_event": { + "canonical_hash": "988d47a4ae347d0b4f49866fc2414acec0139c4be103f3491ae53fd6f2cfe2b2", + "event": { + "actor_id": "30000000-0000-4000-8000-000000000001", + "actor_kind": "user", + "effective_at": "2026-05-21T10:05:00.000000Z", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "reason_code": "operator.requested", + "revoked_at": "2026-05-21T10:05:00.000000Z", + "signature": "ULWxnis0lPSVXh3Hw2di7GUwu/KPflmoKX2PsTkXua2IF53KPduIdYwXy7SkPyERq/FhiEecHBU0qDV0DBVHBw==" + } + }, + "schema": "keel.step4.permit_claim_fixture/v1", + "scope_faithfulness": { + "segments": [ + { + "canonical_filters": { + "canonicalization_profile": "keel.canonical_json.payload.v1", + "filters_hash": "sha256:ce17beeb939a1868b176abcce5003a0dc3f6344d4cdaf3fd47a98eb3a4d067ad", + "raw_filters": { + "equals": { + "event_type": "dispatch.egress_bound", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "operator": "and", + "ranges": { + "occurred_at": { + "gte": "2026-05-21T10:05:00.000000Z", + "lt": "2026-05-21T10:10:00.000000Z" + } + }, + "version": "keel.scope_predicate.v1" + } + }, + "chain_evidence": { + "disclosure_records": [], + "proof_bridge_records": [ + { + "chain_format_version": "v1", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "created_at": "2026-05-21T10:04:00.000000Z", + "event_id": "evt_step4_pre_revocation_dispatch", + "event_type": "dispatch.egress_bound", + "occurred_at": "2026-05-21T10:04:00.000000Z", + "outcome": "success", + "payload_json": { + "dispatch_request_digest_v1": "8d8c75b9d7f2b718abd40045a9880e0a16186df3e2563a651ba7add4a1dafb2c", + "event_type": "dispatch.egress_bound", + "occurred_at": "2026-05-21T10:04:00.000000Z", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "permit_id": "20000000-0000-4000-8000-000000000001", + "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "project_id": "00000000-0000-0000-0000-000000000041", + "record_hash": "3df503d47c612f7250742ebf56efc095c93246204f56c6c13a238570f35f54eb", + "resource_id": "20000000-0000-4000-8000-000000000001", + "resource_type": "permit", + "sequence_number": 1, + "severity": "info" + }, + { + "chain_format_version": "v1", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "created_at": "2026-05-21T10:05:00.000000Z", + "event_id": "evt_step4_permit_revoked", + "event_type": "permit.revoked", + "occurred_at": "2026-05-21T10:05:00.000000Z", + "outcome": "success", + "payload_json": { + "effective_at": "2026-05-21T10:05:00.000000Z", + "event_type": "permit.revoked", + "occurred_at": "2026-05-21T10:05:00.000000Z", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "reason_code": "operator.requested", + "revoked_at": "2026-05-21T10:05:00.000000Z" + }, + "permit_id": "20000000-0000-4000-8000-000000000001", + "prev_hash": "3df503d47c612f7250742ebf56efc095c93246204f56c6c13a238570f35f54eb", + "project_id": "00000000-0000-0000-0000-000000000041", + "record_hash": "051485e92efe5e467f34fcfa91c59cc84752624d4c05e224956a1cc3558afa35", + "resource_id": "20000000-0000-4000-8000-000000000001", + "resource_type": "permit", + "sequence_number": 2, + "severity": "info" + } + ] + }, + "declared_end": { + "boundary_policy": "explicit_checkpoint", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "checkpoint_boundary": "2026-05-21T10:10:00.000000Z", + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "last_record_hash": "051485e92efe5e467f34fcfa91c59cc84752624d4c05e224956a1cc3558afa35", + "sequence_number": 2 + }, + "declared_scope": { + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "population_label": "Post-revocation dispatch.egress_bound events for one permit", + "predicate": { + "equals": { + "event_type": "dispatch.egress_bound", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "operator": "and", + "ranges": { + "occurred_at": { + "gte": "2026-05-21T10:05:00.000000Z", + "lt": "2026-05-21T10:10:00.000000Z" + } + }, + "version": "keel.scope_predicate.v1" + }, + "presentation_policy": { + "policy_kind": "none", + "policy_parameters": {}, + "version": "keel.presentation_policy.v1" + }, + "scope_kind": "declared_sample", + "version": "keel.scope_declaration.v1" + }, + "declared_start": { + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "genesis_prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "kind": "genesis", + "sequence_number": 1 + }, + "scope_state_reference": { + "artifact_hash": "sha256:259a35d5bcbdb44aa2431b9cb7f61e8960f578189835ff2804c169120b6c863e", + "artifact_type": "checkpoint_scope_state", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "scope_state_id": "keel.scope_state.v1:bd32ee1f1b8934ffc3688db3454a7049ebd808e866d2338d9fce48943abc7a92:40000000-0000-4000-8000-000000000001", + "storage_uri": "scope-state/v1/bd32ee1f1b8934ffc3688db3454a7049ebd808e866d2338d9fce48943abc7a92/40000000-0000-4000-8000-000000000001/checkpoint-scope-state-v1.json" + }, + "segment_id": "dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch" + } + ], + "version": "keel.export_scope_faithfulness.v1" + } +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/manifest.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/manifest.json new file mode 100644 index 0000000..049fe07 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/pack/manifest.json @@ -0,0 +1,94 @@ +{ + "claim_set": { + "claims": [ + { + "name": "export.integrity.v1", + "required": true + }, + { + "name": "permit.revoked.v1", + "required": true + }, + { + "name": "checkpoint.scope_state.v1", + "required": true + }, + { + "name": "export.scope_faithfulness.v1", + "required": true + }, + { + "name": "permit.dispatch_absence_after_revocation.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "compressed": false, + "content_hash": "sha256:5ec668937bb69888ad8affa6a9469eb0b551c71fff9995baa9bd17eca07feb49", + "export_id": "dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch", + "export_type": "audit_export", + "format": "json", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "project_id": "00000000-0000-0000-0000-000000000041", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "record_count": 0, + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:d1d67dca7eb9a662d26463c3dec841f47f8791df2fafb21e911dd26a83dabb76", + "id": "keel.export_manifest.integrity.v1", + "path": "semantics/export_manifest/integrity_v1.json" + }, + { + "hash": "sha256:5b7416b11a4a94a2f9f876b2337e118267ab5eb928165cc15f01abaff8639229", + "id": "keel.permit.revoked_event.v1", + "path": "semantics/permit/revoked_event_v1.json" + }, + { + "hash": "sha256:68aafa26d6f1c8cf5ba83c7596209888d8e529d81f1a2c58f31e2fc41fc136de", + "id": "keel.checkpoint.composite_hash.v1", + "path": "semantics/checkpoint/composite_hash_v1.json" + }, + { + "hash": "sha256:af16c66e8a0b295cd2e5e436169bf0e3d628c1fc4901b6eba6596e86e3ad256b", + "id": "keel.checkpoint.signature.v1", + "path": "semantics/checkpoint/signature_v1.json" + }, + { + "hash": "sha256:a3213706c9e9531a74cd2355f2f05e537c7a70604cb869b7b76c65cba4a2b707", + "id": "keel.governance_chain.record_hash.v1", + "path": "semantics/governance_chain/record_hash_v1.json" + }, + { + "hash": "sha256:7fc40790b6d8552b8bff63bbfa69cdd53f744a98be97c217e832ea3299e7b528", + "id": "keel.scope_state.merkle.v1", + "path": "semantics/scope_state/merkle_v1.json" + }, + { + "hash": "sha256:f54ac8a8a0c9fb26ee5870e9aded865376af9dde4899026542e97df5d9f454fd", + "id": "keel.scope_state.sidecar_format.v1", + "path": "semantics/scope_state/sidecar_format_v1.json" + }, + { + "hash": "sha256:478150048a5135ebba4550806a814b27ced491a1198c41ad5a40390045a1435b", + "id": "keel.export.scope_faithfulness.v1", + "path": "semantics/export/scope_faithfulness_v1.json" + }, + { + "hash": "sha256:529f17bf4de5ab0ae4a85b89dd66894ddc65923825defae41d5e8af57d0cc0c4", + "id": "keel.permit.dispatch_absence_after_revocation.v1", + "path": "semantics/permit/dispatch_absence_after_revocation_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:BRxJdYcqeV+wb8CoPbEws/Zaz38wHc61d+qoFk1DB9qGGADWl2gM1E1VnTmieFnPoyp8de4LZsg59AEcox6NDQ==", + "signed_at": "2026-05-21T10:00:00.000000Z" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/sidecars/checkpoint-scope-state-v1.json b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/sidecars/checkpoint-scope-state-v1.json new file mode 100644 index 0000000..743985b --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/dispatch-absence-after-revocation-valid-with-pre-revocation-dispatch/sidecars/checkpoint-scope-state-v1.json @@ -0,0 +1,68 @@ +{ + "artifact_type": "checkpoint_scope_state", + "chain_scope": "project:00000000-0000-0000-0000-000000000041", + "checkpoint_id": "40000000-0000-4000-8000-000000000001", + "commitment_profile": "keel.scope_state.merkle.v1", + "predicate_basis": { + "canonicalization_profile": "keel.canonical_json.payload.v1", + "reserved_namespaces": [ + "keel.scope_predicate.reserved.v1", + "non_membership_profile" + ], + "supported_predicate_kinds": [ + "project_id", + "permit_id", + "request_id", + "event_type", + "category", + "severity", + "decision_type", + "policy_id", + "provider", + "sequence_number", + "created_at", + "occurred_at", + "section", + "export_type" + ] + }, + "predicate_grammar_version": "keel.scope_predicate.v1", + "scope_commitments": [ + { + "first_matching_sequence": null, + "last_matching_sequence": null, + "matching_count": 0, + "membership_root_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "predicate_value": { + "equals": { + "event_type": "dispatch.egress_bound", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041" + }, + "operator": "and", + "ranges": { + "occurred_at": { + "gte": "2026-05-21T10:05:00.000000Z", + "lt": "2026-05-21T10:10:00.000000Z" + } + }, + "version": "keel.scope_predicate.v1" + }, + "predicate_value_hash": "sha256:ce17beeb939a1868b176abcce5003a0dc3f6344d4cdaf3fd47a98eb3a4d067ad" + } + ], + "scope_state_id": "keel.scope_state.v1:bd32ee1f1b8934ffc3688db3454a7049ebd808e866d2338d9fce48943abc7a92:40000000-0000-4000-8000-000000000001", + "signature": { + "algorithm": "Ed25519", + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "signature": "ed25519:uJkbrHfqF8DdWba5c9I7QTajEuoyXSHiTEKCkH5wXbUpEqv8JfgsLrlgYF7M+zYwCY1JM9idC45NyRRbNzrvDg==" + }, + "signed_at": "2026-05-21T10:00:00.000000Z", + "tree_size": 2, + "trust_root_reference": { + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "manifest_version": "keel.public_key_manifest.v1", + "purpose": "scope_state" + }, + "version": "checkpoint_scope_state.v1" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/README.md b/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/README.md new file mode 100644 index 0000000..a235f3d --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/README.md @@ -0,0 +1,11 @@ +# permit-decision-allow-valid + +Valid signed permit allow decision. + +## What It Tests + +The pack contains a signed issuance-time permit decision binding whose canonical payload decision is `allow`. + +## Expected Verdict + +`permit.decision.v1` is expected to return `supported`. diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/pack/export.json b/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/pack/export.json new file mode 100644 index 0000000..a6fb32b --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/pack/export.json @@ -0,0 +1,49 @@ +{ + "fixture_id": "permit-decision-allow-valid", + "permit_decision": { + "artifact_type": "permit_decision_binding", + "artifact_version": "permit.decision.v1", + "binding_canonical_hash": "5e5bab2006d827941aef14338c0e1234649a4d43a633c3e97202cc11996d4bd8", + "binding_issued_at": "2026-05-21T10:00:00.000000Z", + "binding_key_id": "6c8f8607dbe87077", + "binding_signature": "ed25519:80+M167TlR3tEY3f99MmuxOEAnknWlEDfRSVBhTXCOMwhCi4iNNgWadM+U1ybL94nHjCfsK3RZm9RW3mXFLTDg==", + "canonical_payload": { + "action_name": "chat.completion", + "binding_key_id": "6c8f8607dbe87077", + "binding_version": "v1", + "constraints": { + "max_output_tokens": 512, + "temperature_max": 1 + }, + "decision": "allow", + "expires_at": "2026-05-21T11:00:00.000000Z", + "final_request_hash": null, + "is_dry_run": false, + "issued_at": "2026-05-21T10:00:00.000000Z", + "model": "gpt-4o-mini", + "operation": "responses.create", + "parent_permit_id": null, + "permit_id": "10000000-0000-4000-8000-000000000001", + "policy_id": "policy:step4-fixture", + "policy_snapshot_hash": "768eb822105b994a023efd1f9f5933a51354d08c0c4f1051a7818c394aa32de1", + "policy_version": "v2026-05-21", + "project_id": "00000000-0000-0000-0000-000000000041", + "provider": "openai", + "reason": "Fixture allow decision.", + "request_fingerprint": "41241ab40ae41f1e7be2430a2b881d1ea7d51a26465937eb80192b2d31d1e210", + "routing": { + "fallback_chain": [], + "fallback_occurred": false, + "reason_code": "fixture.primary", + "reason_metadata": {}, + "requested_model": "gpt-4o-mini", + "requested_provider": "openai", + "selected_model": "gpt-4o-mini", + "selected_provider": "openai" + } + }, + "expected_decision": "allow" + }, + "project_id": "00000000-0000-0000-0000-000000000041", + "schema": "keel.step4.permit_claim_fixture/v1" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/pack/manifest.json b/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/pack/manifest.json new file mode 100644 index 0000000..36c5805 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-allow-valid/pack/manifest.json @@ -0,0 +1,47 @@ +{ + "claim_set": { + "claims": [ + { + "name": "export.integrity.v1", + "required": true + }, + { + "name": "permit.decision.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "compressed": false, + "content_hash": "sha256:33c3af20d022b8b64e727f60f8dc3cb21e1cfd5c7257f5cc517c000f99820b22", + "export_id": "permit-decision-allow-valid", + "export_type": "audit_export", + "format": "json", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "project_id": "00000000-0000-0000-0000-000000000041", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "record_count": 1, + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:d1d67dca7eb9a662d26463c3dec841f47f8791df2fafb21e911dd26a83dabb76", + "id": "keel.export_manifest.integrity.v1", + "path": "semantics/export_manifest/integrity_v1.json" + }, + { + "hash": "sha256:4fad85a1ab652b6ebc5dd15fd3264025eee400914478dcd4f726c480c34ce70c", + "id": "keel.permit.decision.v1", + "path": "semantics/permit/decision_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:fWs0gkUbF31BEJAqDXvI24g8PgL9jSGHEbIzKmsUKzBk9e9UrsAdHa59uSsBCqzvUYJivdnS4aljgrOKqK6uBA==", + "signed_at": "2026-05-21T10:00:00.000000Z" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/README.md b/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/README.md new file mode 100644 index 0000000..d00d93a --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/README.md @@ -0,0 +1,11 @@ +# permit-decision-challenge-valid + +Valid signed permit challenge decision. + +## What It Tests + +The pack contains a signed issuance-time permit decision binding whose canonical payload decision is `challenge`. + +## Expected Verdict + +`permit.decision.v1` is expected to return `supported`. diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/pack/export.json b/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/pack/export.json new file mode 100644 index 0000000..34c042a --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/pack/export.json @@ -0,0 +1,49 @@ +{ + "fixture_id": "permit-decision-challenge-valid", + "permit_decision": { + "artifact_type": "permit_decision_binding", + "artifact_version": "permit.decision.v1", + "binding_canonical_hash": "41ec76933e21cd57e1035f4e68ca56f80d4981852082bf51386b443be8314e40", + "binding_issued_at": "2026-05-21T10:00:00.000000Z", + "binding_key_id": "6c8f8607dbe87077", + "binding_signature": "ed25519:EUm3VOlDZ/KAyLk7Km5FO3FLMb09BdDtG+VB96gj1GVUM0afogGSMBHqa/8ejNJAtdvcIokD76IysSn9a5TZDw==", + "canonical_payload": { + "action_name": "chat.completion", + "binding_key_id": "6c8f8607dbe87077", + "binding_version": "v1", + "constraints": { + "max_output_tokens": 512, + "temperature_max": 1 + }, + "decision": "challenge", + "expires_at": "2026-05-21T11:00:00.000000Z", + "final_request_hash": null, + "is_dry_run": false, + "issued_at": "2026-05-21T10:00:00.000000Z", + "model": "gpt-4o-mini", + "operation": "responses.create", + "parent_permit_id": null, + "permit_id": "10000000-0000-4000-8000-000000000003", + "policy_id": "policy:step4-fixture", + "policy_snapshot_hash": "97c180c9508bb2b8acf8b27110a472ba242dc334ee7eeafaef5e5fe097e29e03", + "policy_version": "v2026-05-21", + "project_id": "00000000-0000-0000-0000-000000000041", + "provider": "openai", + "reason": "Fixture challenge decision.", + "request_fingerprint": "93692cb071d1d36ffe07071998b542ed434bf2f39066969118714c87d3900688", + "routing": { + "fallback_chain": [], + "fallback_occurred": false, + "reason_code": "fixture.primary", + "reason_metadata": {}, + "requested_model": "gpt-4o-mini", + "requested_provider": "openai", + "selected_model": "gpt-4o-mini", + "selected_provider": "openai" + } + }, + "expected_decision": "challenge" + }, + "project_id": "00000000-0000-0000-0000-000000000041", + "schema": "keel.step4.permit_claim_fixture/v1" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/pack/manifest.json b/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/pack/manifest.json new file mode 100644 index 0000000..2fe2b57 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-challenge-valid/pack/manifest.json @@ -0,0 +1,47 @@ +{ + "claim_set": { + "claims": [ + { + "name": "export.integrity.v1", + "required": true + }, + { + "name": "permit.decision.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "compressed": false, + "content_hash": "sha256:a932324ccb8c1953bfa3864574a848840b5e10ed5fe15510da0d11e901a2fc7e", + "export_id": "permit-decision-challenge-valid", + "export_type": "audit_export", + "format": "json", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "project_id": "00000000-0000-0000-0000-000000000041", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "record_count": 1, + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:d1d67dca7eb9a662d26463c3dec841f47f8791df2fafb21e911dd26a83dabb76", + "id": "keel.export_manifest.integrity.v1", + "path": "semantics/export_manifest/integrity_v1.json" + }, + { + "hash": "sha256:4fad85a1ab652b6ebc5dd15fd3264025eee400914478dcd4f726c480c34ce70c", + "id": "keel.permit.decision.v1", + "path": "semantics/permit/decision_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:Pa6XJx0bQ9vtziGFIp3oaV8YXIj55Iy12ryP6cybB/f8RiTtXcx+EFcrsc4aMimfPjT+9hw+bcMQgupkA5UQBg==", + "signed_at": "2026-05-21T10:00:00.000000Z" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/README.md b/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/README.md new file mode 100644 index 0000000..bc8ecd8 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/README.md @@ -0,0 +1,11 @@ +# permit-decision-deny-valid + +Valid signed permit deny decision. + +## What It Tests + +The pack contains a signed issuance-time permit decision binding whose canonical payload decision is `deny`. + +## Expected Verdict + +`permit.decision.v1` is expected to return `supported`. diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/pack/export.json b/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/pack/export.json new file mode 100644 index 0000000..dda6bbb --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/pack/export.json @@ -0,0 +1,49 @@ +{ + "fixture_id": "permit-decision-deny-valid", + "permit_decision": { + "artifact_type": "permit_decision_binding", + "artifact_version": "permit.decision.v1", + "binding_canonical_hash": "8bb673fd0d4b4c28c9cab53962c945e0a0a4f11e92b77af180bfe54dd1db130d", + "binding_issued_at": "2026-05-21T10:00:00.000000Z", + "binding_key_id": "6c8f8607dbe87077", + "binding_signature": "ed25519:YezOFjx7t8G9UR4CgWn3YwyYiTdBDrNCXWa92lue9ikz1IW/37UrOZRhZUvWn2s4VGFr7mv4DejHcuPv5aBpDA==", + "canonical_payload": { + "action_name": "chat.completion", + "binding_key_id": "6c8f8607dbe87077", + "binding_version": "v1", + "constraints": { + "max_output_tokens": 512, + "temperature_max": 1 + }, + "decision": "deny", + "expires_at": "2026-05-21T11:00:00.000000Z", + "final_request_hash": null, + "is_dry_run": false, + "issued_at": "2026-05-21T10:00:00.000000Z", + "model": "gpt-4o-mini", + "operation": "responses.create", + "parent_permit_id": null, + "permit_id": "10000000-0000-4000-8000-000000000002", + "policy_id": "policy:step4-fixture", + "policy_snapshot_hash": "a07d140afbfd344dbe7cbcd733ac5d5d0664c1d1739a6c93d12db2c8018629da", + "policy_version": "v2026-05-21", + "project_id": "00000000-0000-0000-0000-000000000041", + "provider": "openai", + "reason": "Fixture deny decision.", + "request_fingerprint": "9c0e272b351a6873791e8649c154df699d306956b57b6500d74a77aba078b0e2", + "routing": { + "fallback_chain": [], + "fallback_occurred": false, + "reason_code": "fixture.primary", + "reason_metadata": {}, + "requested_model": "gpt-4o-mini", + "requested_provider": "openai", + "selected_model": "gpt-4o-mini", + "selected_provider": "openai" + } + }, + "expected_decision": "deny" + }, + "project_id": "00000000-0000-0000-0000-000000000041", + "schema": "keel.step4.permit_claim_fixture/v1" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/pack/manifest.json b/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/pack/manifest.json new file mode 100644 index 0000000..5efbb52 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-decision-deny-valid/pack/manifest.json @@ -0,0 +1,47 @@ +{ + "claim_set": { + "claims": [ + { + "name": "export.integrity.v1", + "required": true + }, + { + "name": "permit.decision.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "compressed": false, + "content_hash": "sha256:5bc1fb919d972490a847d89548273bc82ce29f52107f633e67d624237c1fef29", + "export_id": "permit-decision-deny-valid", + "export_type": "audit_export", + "format": "json", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "project_id": "00000000-0000-0000-0000-000000000041", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "record_count": 1, + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:d1d67dca7eb9a662d26463c3dec841f47f8791df2fafb21e911dd26a83dabb76", + "id": "keel.export_manifest.integrity.v1", + "path": "semantics/export_manifest/integrity_v1.json" + }, + { + "hash": "sha256:4fad85a1ab652b6ebc5dd15fd3264025eee400914478dcd4f726c480c34ce70c", + "id": "keel.permit.decision.v1", + "path": "semantics/permit/decision_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:rYFCOgQkbjTwQZGzuV8JHMziMWltPUYCQk+A4cBxFwwBo3Jeg/IV/O1Hcty0/3c+H7cGhngi48xUqp6sm0nACg==", + "signed_at": "2026-05-21T10:00:00.000000Z" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/README.md b/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/README.md new file mode 100644 index 0000000..ed4b4fb --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/README.md @@ -0,0 +1,11 @@ +# permit-revoked-valid + +Valid signed permit revocation event. + +## What It Tests + +The pack contains a signed `permit.revoked` event with `permit_id`, `project_id`, opaque actor identity, taxonomy `reason_code`, and `effective_at == revoked_at`. + +## Expected Verdict + +`permit.revoked.v1` is expected to return `supported`. diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/pack/export.json b/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/pack/export.json new file mode 100644 index 0000000..b4d01f9 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/pack/export.json @@ -0,0 +1,19 @@ +{ + "fixture_id": "permit-revoked-valid", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "revocation_event": { + "canonical_hash": "988d47a4ae347d0b4f49866fc2414acec0139c4be103f3491ae53fd6f2cfe2b2", + "event": { + "actor_id": "30000000-0000-4000-8000-000000000001", + "actor_kind": "user", + "effective_at": "2026-05-21T10:05:00.000000Z", + "permit_id": "20000000-0000-4000-8000-000000000001", + "project_id": "00000000-0000-0000-0000-000000000041", + "reason_code": "operator.requested", + "revoked_at": "2026-05-21T10:05:00.000000Z", + "signature": "ULWxnis0lPSVXh3Hw2di7GUwu/KPflmoKX2PsTkXua2IF53KPduIdYwXy7SkPyERq/FhiEecHBU0qDV0DBVHBw==" + } + }, + "schema": "keel.step4.permit_claim_fixture/v1" +} diff --git a/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/pack/manifest.json b/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/pack/manifest.json new file mode 100644 index 0000000..e98bad6 --- /dev/null +++ b/test-vectors/verifier_claims/v0/fixtures/permit-revoked-valid/pack/manifest.json @@ -0,0 +1,47 @@ +{ + "claim_set": { + "claims": [ + { + "name": "export.integrity.v1", + "required": true + }, + { + "name": "permit.revoked.v1", + "required": true + } + ], + "registry": { + "hash": "sha256:8da29094827fda581ee8fb3a1466934182e572a91b7940d2ef1cb3c28c1ec215", + "id": "keel.verifier_claim_registry.v0", + "path": "claim_registry/v0.json" + }, + "version": "verifier-claims.v0" + }, + "compressed": false, + "content_hash": "sha256:43c9ff644f36e283dd8d337712ed026dd4c6a9780871d25a0ace3a8ccf64df37", + "export_id": "permit-revoked-valid", + "export_type": "audit_export", + "format": "json", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "project_id": "00000000-0000-0000-0000-000000000041", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "record_count": 1, + "semantics_pins": { + "artifacts": [ + { + "hash": "sha256:d1d67dca7eb9a662d26463c3dec841f47f8791df2fafb21e911dd26a83dabb76", + "id": "keel.export_manifest.integrity.v1", + "path": "semantics/export_manifest/integrity_v1.json" + }, + { + "hash": "sha256:5b7416b11a4a94a2f9f876b2337e118267ab5eb928165cc15f01abaff8639229", + "id": "keel.permit.revoked_event.v1", + "path": "semantics/permit/revoked_event_v1.json" + } + ], + "mode": "pinned", + "version": "keel-semantics-pins.v0" + }, + "signature": "ed25519:u5qQPr1MLZJiGEP07jsGBPCDpvAvd3m6STjToT1TDnGvL/YsD94cufL9FvV7eK/mLqSHbdbC0AjuvMABC5a6AQ==", + "signed_at": "2026-05-21T10:00:00.000000Z" +} diff --git a/test-vectors/verifier_claims/v0/trust_roots/step4-permit-claims-trust-root.json b/test-vectors/verifier_claims/v0/trust_roots/step4-permit-claims-trust-root.json new file mode 100644 index 0000000..a5670d5 --- /dev/null +++ b/test-vectors/verifier_claims/v0/trust_roots/step4-permit-claims-trust-root.json @@ -0,0 +1,42 @@ +{ + "generated_at": "2026-05-21T10:00:00.000000Z", + "keys": [ + { + "algorithm": "ed25519", + "key_id": "sha256:10ba682c8ad13513971e8b56881aab8b", + "public_key": "ed25519:0EqyMnQrtKs6E2i9RhXk5tAiSrcaAWuvhSCjMsl3hzc=", + "purpose": "export_signing", + "status": "active", + "valid_from": "2026-01-01T00:00:00Z", + "valid_to": null + }, + { + "algorithm": "ed25519", + "key_id": "6c8f8607dbe87077", + "public_key": "ed25519:F8t5+ytBIPKx7GXkGY1uCLKOgT/rAeSkAIObheGAgM4=", + "purpose": "permit_binding_signing", + "status": "active", + "valid_from": "2026-01-01T00:00:00Z", + "valid_to": null + }, + { + "algorithm": "ed25519", + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "public_key": "ed25519:oJql9HpnWYAv+VX43C0qFKXJnSO+l/hkEn/5ODRVpPA=", + "purpose": "integrity_checkpoint", + "status": "active", + "valid_from": "2026-01-01T00:00:00Z", + "valid_to": null + }, + { + "algorithm": "ed25519", + "key_id": "sha256:1325b850c2871916eae203f0efc3c898", + "public_key": "ed25519:oJql9HpnWYAv+VX43C0qFKXJnSO+l/hkEn/5ODRVpPA=", + "purpose": "scope_state", + "status": "active", + "valid_from": "2026-01-01T00:00:00Z", + "valid_to": null + } + ], + "schema_version": 1 +}