fix(cases): validate optional exact machine descriptor pins
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
8ae5150d4d
commit
015694ee41
6 changed files with 187 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
64
tools/security-case-types/tests/test_descriptor_pins.py
Normal file
64
tools/security-case-types/tests/test_descriptor_pins.py
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue