Prepare typed structural evidence without credential success circularity

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-11 02:42:57 +02:00
parent 247c829569
commit f52c94cd4e
7 changed files with 1094 additions and 27 deletions

View file

@ -0,0 +1,512 @@
"""Source-only structural evidence preparation; no native materialization act.
A construction bundle is not an ownership or native presence receipt. This
module pins it and produces the bounded metadata contract an owner must attest.
Credential mint/delivery and activation never become structural prerequisites.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from contextlib import contextmanager, redirect_stdout, redirect_stderr
import hashlib
import json
import os
import threading
from pathlib import Path, PurePosixPath
import re
from typing import Callable
import yaml
from ops_mason import case_build, case_plan
LIMIT = 262144
OUTPUT_LOCK = threading.RLock()
IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,191}\Z")
DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
class StructureRefused(ValueError):
"""Metadata is incomplete, unverified or not bound to the reviewed structure."""
class _Discard:
def write(self, text):
return len(text)
def flush(self):
pass
@contextmanager
def quiet_owner():
"""Authenticate metadata without retaining owner diagnostics or inherited I/O."""
with OUTPUT_LOCK:
saved = []
null = os.open(os.devnull, os.O_WRONLY)
try:
for fd in (1, 2):
saved.append((fd, os.dup(fd)))
os.dup2(null, fd)
with redirect_stdout(_Discard()), redirect_stderr(_Discard()):
try:
yield
except BaseException:
raise StructureRefused(
"owner structural evidence verification failed"
) from None
finally:
for fd, original in saved:
os.dup2(original, fd)
os.close(original)
os.close(null)
def sha(source: bytes) -> str:
return "sha256:" + hashlib.sha256(source).hexdigest()
def read(path: Path) -> bytes:
if path.is_symlink() or path.stat().st_size > LIMIT:
raise StructureRefused("source path is unsafe or exceeds limit")
return path.read_bytes()
@dataclass(frozen=True)
class StructuralObject:
id: str
kind: str
target: str
expected_digest: str
evidence_owner: str
proof: str
@dataclass(frozen=True)
class Request:
schema: str
case: str
case_digest: str
type_ref: str
type_digest: str
render_digest: str
plan_digest: str
bundle_digest: str
structural_digest: str
reviewed_render_digest: str
run_source_changed: bool
objects: tuple[StructuralObject, ...]
def as_dict(self):
return asdict(self)
@property
def fingerprint(self):
return sha(
json.dumps(self.as_dict(), sort_keys=True, separators=(",", ":")).encode()
)
def prepare(
case_path: Path,
plan_path: Path,
bundle_path: Path,
*,
nk_root=case_plan.DEFAULT_NK_ROOT,
types_root=None,
allow_unimplemented=False,
) -> Request:
"""Use NetKingdom's validator/render; verify every bundle file before emitting."""
case_source, plan_source, manifest_source = (
read(p) for p in (case_path, plan_path, bundle_path / "manifest.json")
)
case, type_doc, rendered = case_plan.render(
case_path, nk_root, types_root, allow_unimplemented
)
root = types_root or nk_root / "security-case-types"
type_source = read(root / type_doc["spec"]["id"] / "type.yaml")
if yaml.safe_load(type_source) != type_doc:
raise StructureRefused("type source changed during preparation")
if yaml.safe_load(case_source) != case:
raise StructureRefused("case source changed during preparation")
try:
manifest = json.loads(manifest_source)
front = yaml.safe_load(plan_source.decode().split("---\n", 2)[1])
provenance = manifest["security_case"]
if (
front["status"] != "reviewed"
or front["render_digest"] != provenance["render_digest"]
):
raise ValueError
if (provenance["case"], provenance["type"]) != (
case["spec"]["id"],
rendered["type"],
):
raise ValueError
if provenance["acceptance_record"] != case_plan.acceptance_ref(case)["record"]:
raise ValueError
files = manifest["files"]
if not isinstance(files, dict) or not 1 <= len(files) <= 64:
raise ValueError
for relative, expected in files.items():
name = PurePosixPath(relative)
if (
name.is_absolute()
or ".." in name.parts
or not name.parts
or name.as_posix() != relative
):
raise ValueError
target = bundle_path / name
if not target.resolve().is_relative_to(bundle_path.resolve()):
raise ValueError
if sha(read(target)) != "sha256:" + expected:
raise ValueError
except (ValueError, KeyError, TypeError, IndexError, yaml.YAMLError):
raise StructureRefused(
"bundle or reviewed plan does not match fresh source"
) from None
# Verify the complete current structural footprint against bundle contents,
# independently of full run prose changes. Never weaken the full run digest.
_match_structural_bundle(case, rendered, bundle_path, files)
structural_digest = sha(
json.dumps(
{
"case": case["spec"]["id"],
"type": rendered["type"],
"acceptance": case_plan.acceptance_ref(case),
"materialization": rendered["materialization"],
},
sort_keys=True,
separators=(",", ":"),
).encode()
)
objects = []
owners = {
"openbao_policy": ("railiance-platform", "native-structure"),
"openbao_role": ("railiance-platform", "native-structure"),
"kv_path": ("railiance-platform", "layout-only"),
"catalog_entry": ("ops-warden", "pointer-source"),
}
for item in rendered["materialization"]:
if item["kind"] not in owners or not all(
IDENTIFIER.fullmatch(item[k]) for k in ("id", "template")
):
raise StructureRefused("unsupported structural object")
owner, proof = owners[item["kind"]]
expected = sha(json.dumps(item, sort_keys=True, separators=(",", ":")).encode())
objects.append(
StructuralObject(
item["id"], item["kind"], item["template"], expected, owner, proof
)
)
if len(objects) > 32 or len({o.id for o in objects}) != len(objects):
raise StructureRefused(
"structural inventory exceeds bounds or repeats identity"
)
return Request(
"ops-mason.case-structure-request.v1",
case["spec"]["id"],
sha(case_source),
rendered["type"],
sha(type_source),
rendered["digest"],
sha(plan_source),
sha(manifest_source),
structural_digest,
provenance["render_digest"],
provenance["render_digest"] != rendered["digest"],
tuple(objects),
)
def _match_structural_bundle(case, rendered, bundle_path, files):
"""Exact generated structure, including self lifecycle; source is not ownership."""
def hcl(text):
return "\n".join(
line.strip()
for line in text.splitlines()
if line.strip() and not line.lstrip().startswith("#")
)
kv = [item for item in rendered["materialization"] if item["kind"] == "kv_path"]
if len(kv) != 1:
raise StructureRefused(
"builder structural contract requires one custody layout"
)
prov = case_build._provenance(case, rendered)
roles = {
yaml.safe_load(item["body"]).get("token_policies", [None])[0]: item
for item in rendered["materialization"]
if item["kind"] == "openbao_role"
}
required = {"rollback.yaml"}
for item in rendered["materialization"]:
if item["kind"] == "openbao_policy":
relative = f"openbao/policies/{item['template']}.hcl"
required.add(relative)
if hcl(read(bundle_path / relative).decode()) != hcl(
case_build._policy_hcl(item, prov)
):
raise StructureRefused(
"current structural policy differs from prepared bundle"
)
expected = case_build._ccr(
case,
rendered,
item,
roles.get(item["template"]),
kv[0]["template"],
prov,
)
relative = f"credential-change-requests/{expected['id']}.yaml"
required.add(relative)
actual = yaml.safe_load(read(bundle_path / relative))
if (
actual.get("openbao") != expected["openbao"]
or actual.get("target") != expected["target"]
or actual.get("request_type") != expected["request_type"]
):
raise StructureRefused(
"current role or reader binding differs from prepared bundle"
)
elif item["kind"] == "catalog_entry":
required.add("catalog-proposal.yaml")
if (
yaml.safe_load(read(bundle_path / "catalog-proposal.yaml")).get("entry")
!= item["body"]
):
raise StructureRefused(
"current catalog pointer differs from prepared bundle"
)
if set(files) != required:
raise StructureRefused(
"bundle inventory contains missing or unsupported source files"
)
rollback = yaml.safe_load(read(bundle_path / "rollback.yaml"))
expected = [
{"kind": item["kind"], "name": item["template"]}
for item in reversed(rendered["materialization"])
]
if rollback.get("removes_exactly") != expected:
raise StructureRefused(
"bundle structural inventory differs from current templates"
)
# These are planned identities, deliberately not creation/undo authority.
@dataclass(frozen=True, slots=True)
class Observation:
object_id: str
owner: str
target: str
expected_digest: str
disposition: str
evidence: str
change_receipt: str | None = None
@dataclass(frozen=True)
class VerifiedStructure:
request_digest: str
created: tuple[StructuralObject, ...]
preexisting: tuple[StructuralObject, ...]
layout_only: tuple[StructuralObject, ...]
pointers: tuple[StructuralObject, ...]
evidence_fingerprints: tuple[str, ...]
@property
def rollback_targets(self):
return tuple(reversed(self.created))
def verify(
request: Request,
observations: tuple[Observation, ...],
verifier: Callable[[Request, StructuralObject, Observation], bool],
) -> VerifiedStructure:
"""Independent authenticated owner/manual evidence required, not trusted JSON.
Returns candidate structural evidence only. No case status or native execution
receipt is issued; this local contract still needs owner seam acknowledgment.
"""
if (
type(request) is not Request
or request.schema != "ops-mason.case-structure-request.v1"
):
raise StructureRefused("unknown structural request protocol")
if not all(
isinstance(value, str) and DIGEST.fullmatch(value)
for value in (
request.case_digest,
request.type_digest,
request.render_digest,
request.plan_digest,
request.bundle_digest,
request.structural_digest,
request.reviewed_render_digest,
)
):
raise StructureRefused("request source pins are malformed")
if (
type(request.case) is not str
or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,127}", request.case)
or type(request.type_ref) is not str
or not re.fullmatch(
r"[a-z0-9][a-z0-9-]{0,127}@[0-9]+\.[0-9]+", request.type_ref
)
):
raise StructureRefused("request case or type identifier malformed")
if type(request.run_source_changed) is not bool or request.run_source_changed != (
request.render_digest != request.reviewed_render_digest
):
raise StructureRefused("execution source change flag disagrees with exact pins")
kinds = {
"openbao_policy": ("railiance-platform", "native-structure"),
"openbao_role": ("railiance-platform", "native-structure"),
"kv_path": ("railiance-platform", "layout-only"),
"catalog_entry": ("ops-warden", "pointer-source"),
}
if type(request.objects) is not tuple or not 1 <= len(request.objects) <= 32:
raise StructureRefused("unknown structural request inventory")
for obj in request.objects:
if type(obj) is not StructuralObject or kinds.get(obj.kind) != (
obj.evidence_owner,
obj.proof,
):
raise StructureRefused("unknown structural proof kind or owner")
if not all(
isinstance(value, str) and IDENTIFIER.fullmatch(value)
for value in (obj.id, obj.target)
) or not DIGEST.fullmatch(obj.expected_digest):
raise StructureRefused("structural metadata or source pin malformed")
if len({obj.id for obj in request.objects}) != len(request.objects):
raise StructureRefused("duplicate structural object identity")
if type(observations) is not tuple or len(observations) != len(request.objects):
raise StructureRefused(
"every structural object needs an exact owner observation"
)
by_id = {o.object_id: o for o in observations if type(o) is Observation}
if len(by_id) != len(observations):
raise StructureRefused("unknown or duplicate observation")
classified = {
"created": [],
"preexisting-matched": [],
"layout-only": [],
"pointer-source": [],
}
fingerprints = []
for obj in request.objects:
observation = by_id.get(obj.id)
if observation is None or (
observation.owner,
observation.target,
observation.expected_digest,
) != (obj.evidence_owner, obj.target, obj.expected_digest):
raise StructureRefused("owner observation scope or source pin mismatch")
if not isinstance(observation.evidence, str) or not IDENTIFIER.fullmatch(
observation.evidence
):
raise StructureRefused("evidence must be a bounded metadata reference")
permitted = (
("created", "preexisting-matched")
if obj.proof == "native-structure"
else (obj.proof,)
)
if observation.disposition not in permitted:
raise StructureRefused(
"credential success or status labels are not structural proof"
)
if observation.disposition == "created":
if not isinstance(
observation.change_receipt, str
) or not IDENTIFIER.fullmatch(observation.change_receipt):
raise StructureRefused(
"creation requires independently verified change receipt"
)
elif observation.change_receipt is not None:
raise StructureRefused("non-created object cannot carry undo authority")
# The provider must authenticate the complete request/object/observation,
# including manual attender identity or owner transport, not just its ref.
try:
with quiet_owner():
authenticated = verifier(request, obj, observation) is True
except Exception:
raise StructureRefused(
"owner structural evidence verification failed"
) from None
if not authenticated:
raise StructureRefused("owner structural evidence unverified")
classified[observation.disposition].append(obj)
fingerprints.append(
sha(json.dumps(asdict(observation), sort_keys=True).encode())
)
return VerifiedStructure(
request.fingerprint,
tuple(classified["created"]),
tuple(classified["preexisting-matched"]),
tuple(classified["layout-only"]),
tuple(classified["pointer-source"]),
tuple(fingerprints),
)
@dataclass(frozen=True)
class StatusBinding:
request_digest: str
accepted_case_digest: str
materialized_case_digest: str
structural_digest: str
owner_evidence_fingerprints: tuple[str, ...]
created: tuple[StructuralObject, ...]
preexisting: tuple[StructuralObject, ...]
def bind_status_transition(
request: Request,
accepted_source: bytes,
materialized_source: bytes,
observations: tuple[Observation, ...],
verifier: Callable[[Request, StructuralObject, Observation], bool],
) -> StatusBinding:
"""Preview a status-only raw-hash binding after independent structural proof.
No file is written and no proof is signed. An admitted owner must authenticate
this binding before Warden accepts it for its new raw case digest.
"""
if (
type(accepted_source) is not bytes
or type(materialized_source) is not bytes
or max(len(accepted_source), len(materialized_source)) > LIMIT
):
raise StructureRefused("case source exceeds bounded transition contract")
if sha(accepted_source) != request.case_digest:
raise StructureRefused("accepted case source pin changed")
try:
before, after = (
yaml.safe_load(accepted_source),
yaml.safe_load(materialized_source),
)
if (
before["spec"]["status"] != "accepted"
or after["spec"]["status"] != "materialized"
):
raise ValueError
before["spec"]["status"] = "materialized"
if before != after:
raise ValueError
except (yaml.YAMLError, KeyError, TypeError, ValueError):
raise StructureRefused(
"only accepted-to-materialized status change is permitted"
) from None
verified = verify(request, observations, verifier)
return StatusBinding(
request.fingerprint,
request.case_digest,
sha(materialized_source),
request.structural_digest,
verified.evidence_fingerprints,
verified.created,
verified.preexisting,
)

View file

@ -37,24 +37,51 @@ def _parser() -> argparse.ArgumentParser:
"--expect-digest", required=True, help="exact digest returned by preflight"
)
apply_parser.add_argument("--readiness-repo", type=Path)
apply_parser.add_argument("--activation", choices=("APPROVED", "BREAK_GLASS"), default="APPROVED")
apply_parser.add_argument(
"--activation", choices=("APPROVED", "BREAK_GLASS"), default="APPROVED"
)
apply_parser.add_argument("--break-glass-reason")
plan = families.add_parser("plan", help="construction plan derived from a SecurityCase")
plan = families.add_parser(
"plan", help="construction plan derived from a SecurityCase"
)
plan.add_argument("--case", required=True, type=Path)
plan.add_argument("--out", type=Path, help="write the plan here (default: stdout)")
plan.add_argument("--no-live", action="store_true",
help="skip the live OpenBao survey (marks the plan's survey source 'none')")
plan.add_argument(
"--no-live",
action="store_true",
help="skip the live OpenBao survey (marks the plan's survey source 'none')",
)
plan.add_argument("--netkingdom-root", type=Path, default=None)
plan.add_argument("--allow-unimplemented", action="store_true",
help="reference case only: render against an acknowledged type")
build = families.add_parser("build", help="build bundle from an accepted SecurityCase and its reviewed plan")
plan.add_argument(
"--allow-unimplemented",
action="store_true",
help="reference case only: render against an acknowledged type",
)
build = families.add_parser(
"build", help="build bundle from an accepted SecurityCase and its reviewed plan"
)
build.add_argument("--case", required=True, type=Path)
build.add_argument("--plan", required=True, type=Path, help="the reviewed plan from plan --case")
build.add_argument("--out", type=Path, help="bundle directory (default: builds/case-<id>)")
build.add_argument("--allow-unimplemented", action="store_true",
help="reference case only: build against an acknowledged type")
build.add_argument(
"--plan", required=True, type=Path, help="the reviewed plan from plan --case"
)
build.add_argument(
"--out", type=Path, help="bundle directory (default: builds/case-<id>)"
)
build.add_argument(
"--allow-unimplemented",
action="store_true",
help="reference case only: build against an acknowledged type",
)
build.add_argument("--netkingdom-root", type=Path, default=None)
structure = families.add_parser(
"structure-request", help="read-only typed structural evidence request"
)
structure.add_argument("--case", required=True, type=Path)
structure.add_argument("--plan", required=True, type=Path)
structure.add_argument("--bundle", required=True, type=Path)
structure.add_argument("--netkingdom-root", type=Path)
structure.add_argument("--allow-unimplemented", action="store_true")
return parser
@ -63,15 +90,39 @@ def _build(args) -> int:
nk_root = args.netkingdom_root or case_plan.DEFAULT_NK_ROOT
try:
case_id = (case_plan.yaml.safe_load(args.case.read_text()) or {}).get("spec", {}).get("id", "unknown")
case_id = (
(case_plan.yaml.safe_load(args.case.read_text()) or {})
.get("spec", {})
.get("id", "unknown")
)
out = args.out or Path("builds") / f"case-{case_id}"
result = case_build.build_from_case(args.case, args.plan, out, nk_root=nk_root,
allow_unimplemented=args.allow_unimplemented)
except (case_plan.CasePlanError, case_build.CaseBuildRefused, FileExistsError, OSError) as exc:
print(json.dumps({"ok": False, "error": str(exc), "issues": getattr(exc, "issues", [])}), file=sys.stderr)
result = case_build.build_from_case(
args.case,
args.plan,
out,
nk_root=nk_root,
allow_unimplemented=args.allow_unimplemented,
)
except (
case_plan.CasePlanError,
case_build.CaseBuildRefused,
FileExistsError,
OSError,
) as exc:
print(
json.dumps(
{"ok": False, "error": str(exc), "issues": getattr(exc, "issues", [])}
),
file=sys.stderr,
)
return 2
fails = case_build.validate_with_platform([Path(p) for p in result["ccrs"]])
print(json.dumps({"ok": not fails, "platform_validation": fails or "pass", **result}, indent=2))
print(
json.dumps(
{"ok": not fails, "platform_validation": fails or "pass", **result},
indent=2,
)
)
return 1 if fails else 0
@ -81,21 +132,66 @@ def _plan(args) -> int:
nk_root = args.netkingdom_root or case_plan.DEFAULT_NK_ROOT
try:
doc, collisions, rendered = case_plan.plan_from_case(
args.case, nk_root=nk_root, structure=case_plan.Structure() if args.no_live else None,
allow_unimplemented=args.allow_unimplemented)
args.case,
nk_root=nk_root,
structure=case_plan.Structure() if args.no_live else None,
allow_unimplemented=args.allow_unimplemented,
)
except case_plan.CasePlanError as exc:
print(json.dumps({"ok": False, "error": str(exc), "issues": exc.issues}), file=sys.stderr)
print(
json.dumps({"ok": False, "error": str(exc), "issues": exc.issues}),
file=sys.stderr,
)
return 2
if args.out:
args.out.write_text(doc)
else:
sys.stdout.write(doc)
print(json.dumps({"ok": not collisions, "digest": rendered["digest"], "collisions": collisions}), file=sys.stderr)
print(
json.dumps(
{
"ok": not collisions,
"digest": rendered["digest"],
"collisions": collisions,
}
),
file=sys.stderr,
)
return 1 if collisions else 0
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
if args.family == "structure-request":
from ops_mason import case_structure, case_plan
try:
request = case_structure.prepare(
args.case,
args.plan,
args.bundle,
nk_root=args.netkingdom_root or case_plan.DEFAULT_NK_ROOT,
allow_unimplemented=args.allow_unimplemented,
)
except Exception:
print(
json.dumps(
{"ok": False, "error": "structural-source-preparation-refused"}
),
file=sys.stderr,
)
return 2
print(
json.dumps(
{
"source_only": True,
"request_digest": request.fingerprint,
"request": request.as_dict(),
},
indent=2,
)
)
return 0
if args.family == "plan":
return _plan(args)
if args.family == "build":