From c93d4a797e220de2e62e77bffd99b3b1148695a2 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:43:12 +0700 Subject: [PATCH] protocol: add Phase 3A verification and CI checks --- .gitea/workflows/verify.yml | 35 +++++++++ Makefile | 43 +++++++++++ gen/protobuf/.gitkeep | 1 + gen/protobuf/phase3a-baseline.binpb | Bin 0 -> 10785 bytes tools/check_scope.py | 114 ++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+) create mode 100644 .gitea/workflows/verify.yml create mode 100644 Makefile create mode 100644 gen/protobuf/.gitkeep create mode 100644 gen/protobuf/phase3a-baseline.binpb create mode 100644 tools/check_scope.py 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 0000000000000000000000000000000000000000..901a121de611253022e6df38291d4abf02029e08 GIT binary patch literal 10785 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 p(1JOz 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)