diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..d4c93cc --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,35 @@ +name: Verify Protocol + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: protocol-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + # The Phase 3A full verifier includes Swift type-checking. The owner must + # provide a macOS runner with the pinned toolchain from the handoff. + runs-on: macos-26 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + - name: Assert pinned toolchain + shell: bash + run: | + python3 --version + go version | grep -F 'go1.26.5 ' + protoc --version | grep -F 'libprotoc 35.1' + buf --version | grep -F '1.72.0' + rustc --version | grep -F '1.97.1' + swift --version | grep -F 'Swift version 6.3.3' + - name: Verify generation, breaking policy, scope, and conformance + run: make verify + - name: Verify clean regeneration + run: git diff --exit-code diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d70bbd1 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile clean-generated + +PYTHON ?= python3 +PROTOC ?= protoc +BUF ?= buf + +generate: + $(PYTHON) tools/generate.py + $(PROTOC) --proto_path=proto --descriptor_set_out=gen/protobuf/control-v1.pb --include_imports --include_source_info proto/versevdi/control/v1/control.proto + $(PROTOC) --proto_path=proto --descriptor_set_out=gen/protobuf/tunnel-v1.pb --include_imports --include_source_info proto/versevdi/tunnel/v1/tunnel.proto + +proto-lint: + $(BUF) lint + +proto-breaking: + $(BUF) breaking proto --against $(abspath gen/protobuf/phase3a-baseline.binpb) --limit-to-input-files + +source-verify: + $(PYTHON) -B tools/validate.py + $(PYTHON) -B tools/fixture_digest.py + +scope-verify: + $(PYTHON) -B tools/check_scope.py + +go-test: + go test ./gen/go/... ./tests/go + +binding-compile: + rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib + swiftc -typecheck gen/swift/Protocol.swift + +conformance: + $(PYTHON) -B tools/fixture_digest.py + go run ./tools/go-conformance + $(PYTHON) tools/run_native_conformance.py + +frame-verify: + $(PYTHON) tools/validate_frames.py + +clean-generated: + $(PYTHON) tools/generate.py --check + +verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile conformance frame-verify clean-generated diff --git a/gen/protobuf/.gitkeep b/gen/protobuf/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/gen/protobuf/.gitkeep @@ -0,0 +1 @@ + diff --git a/gen/protobuf/phase3a-baseline.binpb b/gen/protobuf/phase3a-baseline.binpb new file mode 100644 index 0000000..901a121 Binary files /dev/null and b/gen/protobuf/phase3a-baseline.binpb differ diff --git a/tools/check_scope.py b/tools/check_scope.py new file mode 100644 index 0000000..02ea7e8 --- /dev/null +++ b/tools/check_scope.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Check Phase 3A source boundaries, generated provenance, and secret canaries.""" + +from __future__ import annotations + +import hashlib +import json +import pathlib +import re +import sys + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +FORBIDDEN_WIRE_FIELDS = { + "provider_url", + "vm_address", + "machine_id", + "machine_address", + "direct_host", + "private_key", + "pairing_key", + "rtsp_url", + "gamestream", + "apollo_url", + "windows_password", +} +SECRET_PATTERNS = ( + re.compile(rb"-----BEGIN [A-Z ]+PRIVATE KEY-----"), + re.compile(rb"\bAKIA[0-9A-Z]{16}\b"), + re.compile(rb"\bgh[pousr]_[A-Za-z0-9]{20,}\b"), + re.compile(rb"\bsk-[A-Za-z0-9]{20,}\b"), +) + + +def fail(message: str) -> None: + raise ValueError(message) + + +def check_generated_provenance() -> None: + schema_path = ROOT / "schemas/control-v1.schema.json" + generator_path = ROOT / "tools/generate.py" + manifest = json.loads((ROOT / "gen/manifest.json").read_text(encoding="utf-8")) + schema_hash = hashlib.sha256(schema_path.read_bytes()).hexdigest() + generator_hash = hashlib.sha256(generator_path.read_bytes()).hexdigest() + if manifest.get("schema_sha256") != schema_hash: + fail("generated manifest schema hash is stale") + if manifest.get("generator_sha256") != generator_hash: + fail("generated manifest generator hash is stale") + if manifest.get("protocol_version") != (ROOT / "VERSION").read_text(encoding="utf-8").strip(): + fail("generated manifest protocol version is stale") + + +def check_manifest_schema() -> None: + schema = json.loads((ROOT / "schemas/control-v1.schema.json").read_text(encoding="utf-8")) + definitions = schema.get("$defs", {}) + for name in ("ConnectionManifest", "ManifestGateway", "ManifestTunnel", "ManifestProfile", "ManifestBounds", "GrantReference"): + properties = definitions.get(name, {}).get("properties", {}) + forbidden = sorted(FORBIDDEN_WIRE_FIELDS.intersection(properties)) + if forbidden: + fail(f"{name} exposes forbidden wire fields: {forbidden}") + + +def check_text_boundaries() -> None: + paths = [ + ROOT / "openapi/control-v1.yaml", + ROOT / "schemas/control-v1.schema.json", + ROOT / "proto/versevdi/control/v1/control.proto", + ROOT / "proto/versevdi/tunnel/v1/tunnel.proto", + ROOT / "frames/datagram-v1.md", + ROOT / "frames/registry.json", + ROOT / "registries/features.json", + ROOT / "registries/datagrams.json", + ] + for path in paths: + text = path.read_text(encoding="utf-8").lower() + for field in FORBIDDEN_WIRE_FIELDS: + if field in text: + fail(f"{path.relative_to(ROOT)} contains forbidden wire field {field}") + + generated_paths = list((ROOT / "gen").rglob("*")) + for path in generated_paths: + if not path.is_file() or path.name == "manifest.json" or path.suffix in {".pb", ".binpb"}: + continue + text = path.read_text(encoding="utf-8").lower() + for field in FORBIDDEN_WIRE_FIELDS: + if field in text: + fail(f"generated output {path.relative_to(ROOT)} contains forbidden wire field {field}") + + +def check_secret_canaries() -> None: + for path in ROOT.rglob("*"): + if not path.is_file() or ".git" in path.parts or path.name in {"LICENSE"}: + continue + data = path.read_bytes() + for pattern in SECRET_PATTERNS: + if pattern.search(data): + fail(f"secret canary matched in {path.relative_to(ROOT)}") + + +def main() -> int: + check_generated_provenance() + check_manifest_schema() + check_text_boundaries() + check_secret_canaries() + print("Protocol scope and provenance validation passed") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"scope: {exc}", file=sys.stderr) + raise SystemExit(1)