#!/usr/bin/env python3 """Validate the unpublished Sidecat System 0.1 author-profile fixtures.""" from __future__ import annotations import base64 import hashlib import json import struct import tarfile from pathlib import Path from urllib.parse import urljoin from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from jsonschema import Draft202012Validator, RefResolver, ValidationError HERE = Path(__file__).resolve().parent ROOT = HERE / "fixtures" SCHEMA = json.loads((HERE / "sidecat-author-0.1.schema.json").read_text()) VALIDATOR = Draft202012Validator(SCHEMA) def reject_duplicate_members(pairs: list[tuple[str, object]]) -> dict[str, object]: result: dict[str, object] = {} for key, value in pairs: if key in result: raise ValueError(f"duplicate JSON member: {key}") result[key] = value return result def reject_untagged_float(value: str) -> object: raise ValueError(f"untagged JSON floating number: {value}") def parse_safe_int(value: str) -> int: if value == "-0": raise ValueError("untagged negative zero") result = int(value) if abs(result) > 9007199254740991: raise ValueError("untagged integer exceeds interoperable exact range") return result def reject_non_json_constant(value: str) -> object: raise ValueError(f"non-JSON numeric constant: {value}") def parse_strict(path: Path) -> object: return parse_strict_bytes(path.read_bytes()) def parse_strict_bytes(content: bytes) -> object: raw = content.decode("utf-8", errors="strict") result = json.loads( raw, object_pairs_hook=reject_duplicate_members, parse_int=parse_safe_int, parse_float=reject_untagged_float, parse_constant=reject_non_json_constant, ) validate_unicode_scalars(result) validate_depth(result) return result def validate_unicode_scalars(value: object) -> None: if isinstance(value, str): if any(0xD800 <= ord(char) <= 0xDFFF for char in value): raise ValueError("unpaired UTF-16 surrogate in JSON string") elif isinstance(value, list): for child in value: validate_unicode_scalars(child) elif isinstance(value, dict): for key, child in value.items(): validate_unicode_scalars(key) validate_unicode_scalars(child) def validate_depth(value: object, depth: int = 0) -> None: if depth > 64: raise ValueError("JSON frame exceeds 64 container levels") if isinstance(value, list): for child in value: validate_depth(child, depth + 1) elif isinstance(value, dict): for child in value.values(): validate_depth(child, depth + 1) def b64u_decode(value: str) -> bytes: raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) if base64.urlsafe_b64encode(raw).rstrip(b"=").decode() != value: raise ValueError("noncanonical base64url") return raw def validate_portable_bytes(value: object) -> None: if isinstance(value, list): for child in value: validate_portable_bytes(child) elif isinstance(value, dict): if value.get("$sidecat") == "opaque": data = value["data"] raw = base64.b64decode(data, validate=True) if base64.b64encode(raw).decode() != data: raise ValueError("noncanonical base64") for child in value.values(): validate_portable_bytes(child) def test_auth_preimage() -> bytes: vector = parse_strict(ROOT / "keys/local-ed25519-test-vector.json") seed = b64u_decode(vector["privateSeed"]) if len(seed) != 32: raise ValueError("test Ed25519 seed is not raw32") derived_public = Ed25519PrivateKey.from_private_bytes(seed).public_key().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw, ) if derived_public != b64u_decode(vector["publicKey"]): raise ValueError("test Ed25519 seed and public key differ") def length(text: str) -> bytes: data = text.encode("utf-8") return struct.pack(">H", len(data)) + data preimage = ( b"Sidecat local auth v1\x00" + b64u_decode(vector["serverNonce"]) + b64u_decode(vector["clientNonce"]) + length("sidecat.system/0.1") + length(vector["enrollmentID"]) + length(vector["credentialID"]) ) if b64u_decode(vector["signedPreimage"]) != preimage: raise ValueError("test auth preimage mismatch") return preimage def validate_test_auth(frame: dict[str, object]) -> None: if frame.get("kind") != "authenticate": return vector = parse_strict(ROOT / "keys/local-ed25519-test-vector.json") if frame["credentialID"] != vector["credentialID"]: return # an actual host must resolve and check a different enrolled key if frame["enrollmentID"] != vector["enrollmentID"]: raise ValueError("wrong test enrollment") if frame["clientNonce"] != vector["clientNonce"]: raise ValueError("wrong test client nonce") key = Ed25519PublicKey.from_public_bytes(b64u_decode(vector["publicKey"])) try: key.verify(b64u_decode(frame["signature"]), test_auth_preimage()) except Exception as exc: raise ValueError("bad test harness proof") from exc def validate_owner_match(frame: dict[str, object]) -> None: if frame.get("kind") != "call": return target = frame["target"] if "owner" not in target: return body = frame["input"] if isinstance(body, dict) and isinstance(body.get("ref"), dict): ref = body["ref"] if ref.get("$sidecat") == "object-ref" and ref["owner"] != target["owner"]: raise ValueError("selected target owner differs from input ref owner") def validate_frame(frame: object) -> None: VALIDATOR.validate(frame) validate_portable_bytes(frame) if isinstance(frame, dict): validate_test_auth(frame) validate_owner_match(frame) def validate_descriptor_artifact(descriptor: dict[str, object], fixture_path: Path) -> None: help_path = fixture_path.parent / descriptor["helpPath"] if not help_path.is_file(): raise ValueError(f"missing help: {help_path}") if not help_path.read_text(encoding="utf-8").strip(): raise ValueError("empty package help") interface_ids: set[str] = set() for interface in descriptor["interfaces"]: if interface["id"] in interface_ids: raise ValueError("duplicate interface ID") interface_ids.add(interface["id"]) names = [operation["name"] for operation in interface["operations"]] if len(names) != len(set(names)): raise ValueError("duplicate operation name") for operation in interface["operations"]: for key in ("inputSchema", "outputSchema"): path = fixture_path.parent / operation[key] if not path.is_file(): raise ValueError(f"missing {key}: {path}") Draft202012Validator.check_schema(parse_strict(path)) if operation["retry"] == "deduplicated": if operation["retryStatusOperation"] not in names: raise ValueError("missing retry status operation") event_names = [event["name"] for event in interface["events"]] if len(event_names) != len(set(event_names)): raise ValueError("duplicate event name") for event in interface["events"]: path = fixture_path.parent / event["schema"] if not path.is_file(): raise ValueError(f"missing event schema: {path}") Draft202012Validator.check_schema(parse_strict(path)) def validate_contact_payloads() -> None: pos = ROOT / "positive" descriptor_path = pos / "contact-descriptor.json" validate_descriptor_artifact(parse_strict(descriptor_path), descriptor_path) call = parse_strict(pos / "contact-replace-call.json") output = parse_strict(pos / "completed-outcome.json") event = parse_strict(pos / "retained-event.json") status = parse_strict(pos / "replace-status-outcome.json") package_base = "https://package.invalid/contact/" packaged_bytes: dict[str, bytes] = {} with tarfile.open(pos / "contact-package.test.tar", "r") as archive: for member in archive.getmembers(): content = archive.extractfile(member) if content is None: raise ValueError(f"unreadable package member: {member.name}") packaged_bytes[member.name] = content.read() packaged_schemas: dict[str, object] = {} for name, content in packaged_bytes.items(): if not name.startswith("schemas/") or not name.endswith(".json"): continue schema = parse_strict_bytes(content) Draft202012Validator.check_schema(schema) packaged_schemas[urljoin(package_base, name)] = schema if "$id" in schema: packaged_schemas[schema["$id"]] = schema def no_network(uri: str) -> object: raise ValueError(f"schema import attempted network access: {uri}") descriptor = parse_strict_bytes(packaged_bytes["contact-descriptor.json"]) referenced = {descriptor["helpPath"]} for interface in descriptor["interfaces"]: for operation in interface["operations"]: referenced.update((operation["inputSchema"], operation["outputSchema"])) referenced.update(item["schema"] for item in interface["events"]) missing = referenced.difference(packaged_bytes) if missing: raise ValueError(f"descriptor references absent package members: {sorted(missing)}") event_declaration = next(item for item in descriptor["interfaces"][0]["events"] if item["name"] == event["event"]) event_schema_bytes = packaged_bytes[event_declaration["schema"]] if event["eventVersion"] != event_declaration["version"]: raise ValueError("retained event does not name its original schema version") if event["schemaDigest"] != hashlib.sha256(event_schema_bytes).hexdigest(): raise ValueError("retained event does not bind its original schema bytes") checks = [ ("read-input.json", {"ref": call["input"]["ref"]}), ("replace-input.json", call["input"]), ("replace-output.json", output["value"]), ("contact-changed.json", event["payload"]), ("replace-status-output.json", status["value"]), ] for filename, value in checks: path = f"schemas/{filename}" schema = parse_strict_bytes(packaged_bytes[path]) resolver = RefResolver( base_uri=urljoin(package_base, path), referrer=schema, store=packaged_schemas, handlers={"http": no_network, "https": no_network}, ) Draft202012Validator(schema, resolver=resolver).validate(value) def fixture_fingerprint(call: dict[str, object]) -> str: # These fixtures use ASCII keys/strings and no raw floating numbers. This # small calculation is not a general RFC 8785 implementation or qualification. selected = { "target": call["target"], "input": call["input"], "requestedGuarantees": call.get("requestedGuarantees", []), } data = json.dumps(selected, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() return hashlib.sha256(data).hexdigest() def validate_bindings() -> None: pos = ROOT / "positive" invoice = parse_strict(pos / "nested-domain-call.json") if invoice["target"]["owner"] == invoice["input"]["customer"]["owner"]: raise ValueError("cross-app fixture must keep invoice and contact owners distinct") activation = parse_strict(pos / "activation.json") for key, path in ( ("packageDigest", pos / "contact-package.test.tar"), ("descriptorDigest", pos / "contact-descriptor.json"), ("handlerBuildDigest", pos / "handler-build.test-bytes"), ): if activation[key]["digest"] != hashlib.sha256(path.read_bytes()).hexdigest(): raise ValueError(f"activation {key} does not bind current fixture bytes") with tarfile.open(pos / "contact-package.test.tar", "r") as tar: members = tar.getmembers() expected = { "contact-descriptor.json", "handler-build.test-bytes", "docs/help.md", *(f"schemas/{path.name}" for path in (pos / "schemas").glob("*.json")), } if {member.name for member in members} != expected: raise ValueError("activation package member set differs from current fixture") for member in members: source = pos / member.name content = tar.extractfile(member) if content is None or content.read() != source.read_bytes(): raise ValueError(f"activation package member is stale: {member.name}") challenge = parse_strict(pos / "challenge.json") authenticate = parse_strict(pos / "authenticate.json") ready = parse_strict(pos / "ready.json") vector = parse_strict(ROOT / "keys/local-ed25519-test-vector.json") if challenge["serverNonce"] != vector["serverNonce"]: raise ValueError("challenge differs from signed preimage") if authenticate["enrollmentID"] != ready["enrollmentID"]: raise ValueError("ready binds a different enrollment") prepared = parse_strict(pos / "local-harness-profile.json") if prepared.get("expectedActor") != ready["actorID"]: raise ValueError("prepared profile and ready actor differ") if ready["maxFrameBytes"] > challenge["maxFrameBytes"]: raise ValueError("ready exceeds challenged frame bound") wrong_launch = parse_strict(ROOT / "launch/valid-sibling-wrong-actor.json") if wrong_launch["expectedActor"] == wrong_launch["authenticatedReadyActor"] or wrong_launch["expected"] != "refuse-before-call": raise ValueError("wrong-profile launch case no longer detects actor misbinding") descriptor = parse_strict(pos / "contact-descriptor.json") if activation["appID"] != descriptor["appID"]: raise ValueError("activation app mismatch") if activation["interfaces"][0]["id"] != descriptor["interfaces"][0]["id"]: raise ValueError("activation interface mismatch") provider = descriptor["interfaces"][0] requirement_validator = Draft202012Validator( SCHEMA["$defs"]["requirement"], resolver=RefResolver.from_schema(SCHEMA), ) for path in sorted((ROOT / "requirements").glob("*.json")): case = parse_strict(path) required = case["requirement"] requirement_validator.validate(required) if required["interface"] != provider["id"] or required["major"] != provider["major"]: actual = "incompatible-interface" elif provider["minor"] < required["minimumMinor"]: actual = "incompatible-minor" elif not set(required["requiredProfiles"]).issubset(provider["profiles"]): actual = "unsupported-profile" else: actual = "satisfied" if actual != case["expected"]: raise ValueError(f"requirement resolution differs for {path.name}: {actual}") gated = parse_strict(pos / "gated-contact-replace-call.json") selected = parse_strict(pos / "selection.json") if selected["candidateFingerprint"] != fixture_fingerprint(gated): raise ValueError("assurance selection targets different call content") if selected["effectiveGuarantees"] != gated["requestedGuarantees"]: raise ValueError("assurance selection lost requested guarantee") ordinary = parse_strict(pos / "contact-replace-call.json") status = parse_strict(pos / "replace-status-outcome.json") if status["value"]["requestFingerprint"] != fixture_fingerprint(ordinary): raise ValueError("retry status lost original request identity") def main() -> None: Draft202012Validator.check_schema(SCHEMA) over_deep: object = None for _ in range(65): over_deep = [over_deep] try: validate_depth(over_deep) except ValueError: pass else: raise AssertionError("deeply nested JSON frame unexpectedly accepted") preparation_path = ROOT / "positive/local-harness-profile.json" preparation_schema = parse_strict(HERE / "local-harness-preparation-0.1.schema.json") Draft202012Validator.check_schema(preparation_schema) preparation = parse_strict(preparation_path) Draft202012Validator(preparation_schema).validate(preparation) vector = parse_strict(ROOT / "keys/local-ed25519-test-vector.json") if (preparation["enrollmentID"], preparation["credentialID"]) != (vector["enrollmentID"], vector["credentialID"]): raise ValueError("prepared profile differs from authenticated credential") positive = sorted(path for path in (ROOT / "positive").glob("*.json") if path != preparation_path) negative = sorted((ROOT / "negative").glob("*.json")) for path in positive: validate_frame(parse_strict(path)) for path in negative: try: validate_frame(parse_strict(path)) except (ValueError, UnicodeError, json.JSONDecodeError, ValidationError): continue raise AssertionError(f"negative fixture unexpectedly passed: {path.name}") validate_contact_payloads() validate_bindings() bad = parse_strict(ROOT / "negative/dedupe-without-status.json") descriptor_validator = Draft202012Validator( SCHEMA["$defs"]["descriptor"], resolver=RefResolver.from_schema(SCHEMA), ) errors = list(descriptor_validator.iter_errors(bad)) if len(errors) != 1 or "retryStatusOperation" not in errors[0].message: raise AssertionError("dedupe negative no longer isolates the retry-status rule") print(f"schema and semantic bindings valid; {len(positive)} positive and {len(negative)} negative frames passed") print("offline package schemas, dependency resolution, event versions, local proof/preparation and wrong-profile case, package hashes, assurance and retry checked") if __name__ == "__main__": main()