From 015694ee414c802dc2ef1fa0120dfed610086e69 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 16 Oct 2026 21:23:04 +0200 Subject: [PATCH] fix(cases): validate optional exact machine descriptor pins Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786 --- .../security-case-type_v0.1.schema.json | 18 ++++++ tools/security-case-types/README.md | 15 +++++ .../security_case_evidence.py | 5 ++ .../security_case_validator.py | 25 ++++++++ .../tests/test_descriptor_pins.py | 64 +++++++++++++++++++ workplans/NK-WP-0048-security-case-types.md | 62 +++++++++++++++++- 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 tools/security-case-types/tests/test_descriptor_pins.py diff --git a/canon/schemas/security-case-type_v0.1.schema.json b/canon/schemas/security-case-type_v0.1.schema.json index 22ab31c..3575d8a 100644 --- a/canon/schemas/security-case-type_v0.1.schema.json +++ b/canon/schemas/security-case-type_v0.1.schema.json @@ -132,6 +132,24 @@ "items": { "type": "object" } + }, + "execution_descriptors": { + "type": "object", + "maxProperties": 128, + "propertyNames": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$", + "not": { + "pattern": "[^a-z0-9-]" + } + }, + "additionalProperties": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "minLength": 71, + "maxLength": 71 + }, + "description": "Optional exact owner machine-descriptor byte digests keyed by declared follow-through step. A pin alone is not owner acknowledgment, execution authority or native evidence." } } } diff --git a/tools/security-case-types/README.md b/tools/security-case-types/README.md index dc4fc91..d25d2e9 100644 --- a/tools/security-case-types/README.md +++ b/tools/security-case-types/README.md @@ -95,3 +95,18 @@ Integration tests invoke actual sibling Mason `prepare` and Warden `Journal.save with metadata fixtures only. They need those owner source checkouts, and existing type suites need the public KeyCape request file and provider declarations beside the checkout. `NET_KINGDOM_ROOT` selects the source root for the 1.2 package tests. + +## Machine descriptor source pins + +The optional `spec.execution_descriptors` map uses the existing Warden kernel's +step-to-exact-descriptor-byte SHA-256 contract. The JSON schema accepts at most +128 lowercase `sha256:<64 hex>` pins. The semantic validator additionally requires +every key to name a declared follow-through step. Malformed maps are refused by +the direct evidence API too. Absent and partial maps remain legal for source +preparation; missing steps remain explicit in the evidence report. + +A valid pin is source metadata. It does not authenticate its owner, acknowledge +a machine seam, admit an adapter or change the implemented-type requirement. +The existing browser types contain no descriptors; their bytes and lifecycle +states are unchanged. Owner descriptor/provenance acceptance and actual native +receipts remain prerequisites to execution and acceptance. diff --git a/tools/security-case-types/security_case_evidence.py b/tools/security-case-types/security_case_evidence.py index 0bf0a84..5f25577 100644 --- a/tools/security-case-types/security_case_evidence.py +++ b/tools/security-case-types/security_case_evidence.py @@ -135,6 +135,11 @@ def check_steps(rows, case_source, type_source, rendered): "journal_schema", "evidence_digest", "session_revoked", "session_revocation_digest", "grant_revocation_digest", "attempt_id", "operation"} type_spec = yaml.safe_load(type_source)["spec"] + # The direct API must refuse malformed maps too, without AttributeError or + # treating an undeclared step as an admitted descriptor. + from security_case_validator import validate_execution_descriptors + if validate_execution_descriptors(type_spec): + raise EvidenceRefused("invalid type machine descriptor pins") descriptors = type_spec.get("execution_descriptors", {}) seen, result = set(), [] for row in rows: diff --git a/tools/security-case-types/security_case_validator.py b/tools/security-case-types/security_case_validator.py index 1dc63d6..06cfff4 100644 --- a/tools/security-case-types/security_case_validator.py +++ b/tools/security-case-types/security_case_validator.py @@ -151,6 +151,7 @@ def validate_type(data: dict[str, Any], type_dir: Path | None = None) -> list[Is params = _validate_type_parameters(spec["parameters"], issues) step_ids = _validate_follow_through(spec["follow_through"], issues) + issues.extend(validate_execution_descriptors(spec)) _validate_composition(spec["composition"], params, issues) _validate_materialization(spec["materialization"], params, issues) _validate_verification(spec["verification"], params, issues) @@ -190,6 +191,30 @@ def validate_type(data: dict[str, Any], type_dir: Path | None = None) -> list[Is return issues +def validate_execution_descriptors(spec: dict[str, Any]) -> list[Issue]: + """Validate optional source pins; never infer acknowledgment or authority. + + Incomplete/absent maps remain legal for plan-only types. Admission and + provenance remain the executing owner's responsibility. + """ + if "execution_descriptors" not in spec: + return [] + path = "type.spec.execution_descriptors" + pins = spec["execution_descriptors"] + if type(pins) is not dict or len(pins) > 128: + return [err(path, "must be a mapping of at most 128 step descriptor pins")] + steps = spec.get("follow_through") + declared = {step.get("id") for step in steps + if type(step) is dict and type(step.get("id")) is str} if type(steps) is list else set() + issues = [] + for step, pin in pins.items(): + if type(step) is not str or not ID.fullmatch(step) or step not in declared: + issues.append(err(path, "descriptor key must identify a declared follow-through step")) + if type(pin) is not str or re.fullmatch(r"sha256:[0-9a-f]{64}", pin) is None: + issues.append(err(path, "descriptor pin must be a lowercase SHA-256 byte digest")) + return issues + + def _validate_type_parameters(params: Any, issues: list[Issue]) -> dict[str, dict]: out: dict[str, dict] = {} if not isinstance(params, list) or not params: diff --git a/tools/security-case-types/tests/test_descriptor_pins.py b/tools/security-case-types/tests/test_descriptor_pins.py new file mode 100644 index 0000000..8980425 --- /dev/null +++ b/tools/security-case-types/tests/test_descriptor_pins.py @@ -0,0 +1,64 @@ +"""Owner pin contract parity; synthetic pins never establish native acceptance.""" +import copy +import json +from pathlib import Path +import sys + +import jsonschema +import pytest +import yaml + +TOOL = Path(__file__).resolve().parents[1] +REPO = TOOL.parents[1] +sys.path.insert(0, str(TOOL)) +import security_case_validator as v +import security_case_evidence as e + +TYPE = yaml.safe_load((REPO / 'security-case-types/_template/type.yaml').read_bytes()) +SCHEMA = json.loads((REPO / 'canon/schemas/security-case-type_v0.1.schema.json').read_bytes()) +PIN = 'sha256:' + 'a' * 64 +STEP = TYPE['spec']['follow_through'][0]['id'] + + +def with_pins(pins): + doc = copy.deepcopy(TYPE) + doc['spec']['execution_descriptors'] = pins + return doc + + +@pytest.mark.parametrize('pins', [{}, {STEP: PIN}]) +def test_optional_partial_pin_contract_has_schema_semantic_parity(pins): + doc = with_pins(pins) + jsonschema.validate(doc, SCHEMA) + assert not v.has_errors(v.validate_type(doc)) + assert doc['spec']['status'] == TYPE['spec']['status'] + + +@pytest.mark.parametrize('pins', [None, [], 'digest', {STEP: True}, {STEP: None}, + {STEP: 17}, {STEP: 'a' * 64}, {STEP: PIN.upper()}, {STEP: PIN + '\n'}, + {STEP + '\n': PIN}, {7: PIN}, {'../escape': PIN}, + {f'step-{i}': PIN for i in range(129)}]) +def test_malformed_descriptor_contract_refused_by_both_validators(pins): + doc = with_pins(pins) + assert v.has_errors(v.validate_type(doc)) + with pytest.raises(jsonschema.ValidationError): + jsonschema.validate(doc, SCHEMA) + + +def test_well_shaped_unknown_step_is_semantic_refusal(): + doc = with_pins({'not-a-declared-step': PIN}) + jsonschema.validate(doc, SCHEMA) + assert v.has_errors(v.validate_type(doc)) + + +@pytest.mark.parametrize('pins', [None, [], {STEP: 'bad'}, {'undeclared-step': PIN}]) +def test_direct_evidence_api_refuses_bad_map_without_crashing_or_needing_rows(pins): + with pytest.raises(e.EvidenceRefused, match='machine descriptor pins'): + e.check_steps((), b'case', yaml.safe_dump(with_pins(pins)).encode(), {'follow_through': []}) + + +def test_partial_pin_cannot_bypass_ordinary_case_admission(): + doc = with_pins({STEP: PIN}) + case = yaml.safe_load((REPO / 'security-case-types/_template/examples/example-operator.yaml').read_bytes()) + issues = v.validate_case(case, doc) + assert any('only implemented types' in issue.message for issue in issues) diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md index 074efd1..72f1a9b 100644 --- a/workplans/NK-WP-0048-security-case-types.md +++ b/workplans/NK-WP-0048-security-case-types.md @@ -9,7 +9,7 @@ flavor: implementation owner: claude-code topic_slug: netkingdom created: "2026-10-08" -updated: "2026-10-10" +updated: "2026-10-16" depends_on: [] related_repos: - ops-mason @@ -323,7 +323,7 @@ the rationale. The type is `acknowledged`; it becomes `implemented` with T06. ```task id: NK-WP-0048-T06 status: wait -depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, MASON-WP-0010-T03, WARDEN-WP-0044-T03] +depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, MASON-WP-0010-T03, WARDEN-WP-0044-T03] blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status." priority: high state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c" @@ -584,3 +584,61 @@ validator, then project only descriptor-declared safe target metadata; do not remove the credential/value refusal or copy complete parameters into journals. Actual owner descriptor acknowledgment and independent evidence admission remain prerequisites, with normal admission still refusing unimplemented types. + + +## T10 — Source compatibility checker for Mason and Warden evidence + +```task +id: NK-WP-0048-T10 +status: done +priority: high +``` + +Completed 2026-10-16 in published source `c31d21ba`. The read-only +`check-evidence` command checks the actual Mason structural request and Warden +journal source protocols against an exact version/render. Independent source +review passed 103 tests at that checkpoint. It always reports native completion +false and execution prohibited. Fixtures and local journal fingerprints do not +provide authenticated owner/native evidence. This records completed source scope +previously described under T06; it does not duplicate native acceptance credit. + +## T11 — Validate the optional machine-descriptor pin contract + +```task +id: NK-WP-0048-T11 +status: done +priority: high +``` + +Completed 2026-10-16. Reproduced the JSON schema rejecting a valid +`execution_descriptors` map while the Python validator accepted malformed maps. +The schema now supports the existing Warden optional exact-byte pin map; semantic +validation binds keys to declared steps, bounds the map and checks digest shape. +The direct evidence API refuses malformed maps before accessing them. Twenty-one +new checks and all 127 affected source checks passed. No current type/case bytes, +status, owner descriptors or native acceptance changed. Source evidence and +publication receipt: Custodian +`docs/changes/netkingdom-target-2026-10-16/browser-acceptance-next/`. + +## T12 — Prepare NetKingdom-owned attended step adapters + +```task +id: NK-WP-0048-T12 +status: todo +priority: high +``` + +Owner: NetKingdom. Next action: wrap the existing contained `keycape-client.py` +registration/config/env and identity-provisioner ensure/revoke entry points in +bounded owner adapters or receipt-only attended steps for Warden's closed kernel. +Reuse the existing caller/session boundary, CAS/readback and status-only outputs; +never acquire user-engine's runtime identity or expose registry/client secrets. +Completion requires exact adapter/source descriptors, positive and refusal source +checks, uncertainty/created-only rollback behavior and a separate owner review of +the proposed machine seam. Source preparation can begin without credentials. +Warden independently owns global provenance/admission integration; Platform owns +mint/broker and native structural receipts; Apps owns consumer delivery/group +acceptance. Their assent is not supplied by this task. Pinning descriptors into +an acknowledged type remains a separately reviewed type-source change. Actual +attended execution still requires its existing owner authorization and native +receipts. T06 retains all native/reference lifecycle acceptance scope.