Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,49 @@ name: CI

on:
push:
branches: [ main ]
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync --all-extras --dev

- name: Install uv
uses: astral-sh/setup-uv@v4
- run: make check

# Also installs the Python version pinned in .python-version.
- name: Install dependencies
run: uv sync --all-extras --dev
live-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync --all-extras --dev

# Without the binary the registry runtime-code pin test skips, and a
# drift-guard job that always skips is dead weight. Built from enclave's
# moving seismic branch — which is the point (guard against enclave
# HEAD), and why it stays out of the required `check` job: the crate is
# small (none of the enclave workspace's TPM deps), and the cache
# re-keys only when enclave moves.
- name: Resolve enclave HEAD
id: enclave
run: >-
echo "rev=$(git ls-remote https://github.com/SeismicSystems/enclave.git
refs/heads/seismic | cut -f1)" >> "$GITHUB_OUTPUT"
- name: Cache seismic-measurement-admission
id: admission-cache
uses: actions/cache@v4
with:
path: ~/.cargo/bin/seismic-measurement-admission
key: seismic-measurement-admission-${{ steps.enclave.outputs.rev }}
- name: Build seismic-measurement-admission
if: steps.admission-cache.outputs.cache-hit != 'true'
run: >-
cargo install --locked
--git https://github.com/SeismicSystems/enclave
--rev ${{ steps.enclave.outputs.rev }}
seismic-measurement-admission --features cli

- name: make check
run: make check
- run: make test-live
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

PKG := deploy_gcp tee

.PHONY: help lint format format-check typecheck test check
.PHONY: help lint format format-check typecheck test test-live check

help:
@echo "Targets:"
@echo " check lint + format-check + typecheck + test (what CI runs)"
@echo " check lint + format-check + typecheck + test (hermetic; CI runs this)"
@echo " lint ruff check"
@echo " format ruff format (writes changes)"
@echo " format-check ruff format --check"
@echo " typecheck ty check"
@echo " test unittest discovery (repo-wide)"
@echo " test unittest discovery (repo-wide, offline)"
@echo " test-live cross-repo drift guards (network-required; own CI step)"

check: lint format-check typecheck test

Expand All @@ -31,3 +32,8 @@ typecheck:
# passing runs stay quiet (the code under test prints progress + warnings).
test:
uv run python -m unittest discover -b

# Live cross-repo drift guards: modules named live_test_*.py fall outside
# `test`'s default `test*.py` discovery, keeping it hermetic.
test-live:
uv run python -m unittest discover -b -v -p "live_test_*.py"
112 changes: 112 additions & 0 deletions tee/cli/common/tests/live_test_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Live cross-repo drift guards (network-required; run via `make test-live`).

These tests fetch pinned artifacts from sibling repos on GitHub and compare
them against what this repo renders or pins. The module name deliberately
does not match `make test`'s `test*.py` discovery pattern, so the default
suite stays hermetic (offline, deterministic); CI runs this module as its
own non-required job, where a failure names the exact cross-repo check.

The suite never skips — a missing prerequisite is a failure. It needs:

- network reach to raw.githubusercontent.com (both tests fetch pinned
artifacts from sibling repos);
- `seismic-measurement-admission` on PATH — the enclave repo's admission
CLI (`cargo install --features cli` from crates/measurement-admission;
CI builds it from enclave's seismic branch).

Run with:
make test-live
"""

import http.client
import json
import unittest
import urllib.error
import urllib.request

from eth_utils import keccak

from tee.cli.common.manifest import compile_measurement_policy, render_manifest
from tee.cli.common.tests.test_manifest import (
ADMISSION_BIN,
FIXTURE_MANIFEST,
promoted_policy_bytes,
)


def _fetch_live(url: str) -> bytes:
"""Fetch a cross-repo artifact, failing the calling test if it can't.

An HTTP 4xx/5xx means the artifact moved or the ref is gone — a real
drift signal, not flaky network — so it propagates directly. A
transport-level failure (unreachable, timeout, truncated body) is
retried once, then fails: this suite never skips.
"""
last: Exception | None = None
for _ in range(2):
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return resp.read()
except urllib.error.HTTPError:
raise
except (urllib.error.URLError, TimeoutError, http.client.HTTPException) as e:
last = e
raise AssertionError(
f"cross-repo artifact unreachable after retry: {last}"
) from last


class ManifestFixtureParityTests(unittest.TestCase):
"""Byte-parity with the node-side manifest parser.

The enclave repo pins the manifest fixture's exact bytes; deploy's
emitter must render the same dict to the same bytes. Fetched from
GitHub (the `seismic` branch) rather than assuming a sibling checkout
on disk, so the check runs in CI too. The fixture's network_id is also
pinned offline by test_manifest's
test_render_matches_enclave_network_id_vector; this adds the live
byte-level drift guard on top.
"""

ENCLAVE_FIXTURE_URL = (
"https://raw.githubusercontent.com/SeismicSystems/enclave/seismic/"
"crates/network-manifest/fixtures/network-manifest-v1.json"
)

def test_render_matches_enclave_fixture_bytes(self):
fixture = _fetch_live(self.ENCLAVE_FIXTURE_URL)
self.assertEqual(render_manifest(FIXTURE_MANIFEST), fixture)


class RuntimeCodeDriftTests(unittest.TestCase):
"""Cross-repo drift guard for the registry runtime-code pin.

The admission CLI pins keccak256 of the canonical MeasurementRegistry
deployed bytecode; the gates enforce that pin against the genesis alloc,
so a stale pin already fails assembly loudly. This test is the early
warning: the pin reported by the binary on PATH must match the artifact
the reth genesis builder installs.
"""

REGISTRY_ARTIFACT_URL = (
"https://raw.githubusercontent.com/SeismicSystems/seismic/main/"
"contracts/artifacts/MeasurementRegistry.json"
)

def test_admission_crate_pins_current_registry_runtime(self):
self.assertIsNotNone(
ADMISSION_BIN,
"seismic-measurement-admission not on PATH — the live suite fails "
"rather than skips; build the enclave repo's admission CLI",
)
report = compile_measurement_policy(promoted_policy_bytes())
artifact = json.loads(_fetch_live(self.REGISTRY_ARTIFACT_URL))
runtime = artifact["deployedBytecode"]["object"].removeprefix("0x")
self.assertEqual(
report["registry_runtime_code_hash"],
"0x" + keccak(bytes.fromhex(runtime)).hex(),
)


if __name__ == "__main__":
unittest.main()
71 changes: 6 additions & 65 deletions tee/cli/common/tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@
"""

import hashlib
import http.client
import json
import shutil
import tempfile
import tomllib
import unittest
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -104,39 +101,16 @@ def promoted_policy_bytes(measurement_id: str = "img.vhd") -> bytes:
"0x8ef142e3f2bf15f8b201c4d8cda7848a9e846222c62b5615d4d36c7fccd98a24"
)

# The node-side parser pins these exact bytes in the enclave repo. Fetch its
# fixture from GitHub (the `seismic` branch) rather than assuming a sibling
# checkout on disk, so the cross-repo byte-parity check runs in CI too. The
# network_id value is also pinned offline by
# test_render_matches_enclave_network_id_vector, so this only adds a live drift
# guard; it skips when GitHub is unreachable.
ENCLAVE_FIXTURE_URL = (
"https://raw.githubusercontent.com/SeismicSystems/enclave/seismic/"
"crates/network-manifest/fixtures/network-manifest-v1.json"
)


def _fetch_enclave_fixture() -> bytes:
with urllib.request.urlopen(ENCLAVE_FIXTURE_URL, timeout=10) as resp:
return resp.read()
# The node-side parser pins the fixture's exact bytes in the enclave repo;
# the live byte-parity check against it lives in live_test_manifest.py
# (network-required, `make test-live`).


class RenderTests(unittest.TestCase):
def test_render_matches_enclave_network_id_vector(self):
rendered = render_manifest(FIXTURE_MANIFEST)
self.assertEqual(compute_network_id(rendered), FIXTURE_NETWORK_ID)

def test_render_matches_enclave_fixture_bytes(self):
try:
fixture = _fetch_enclave_fixture()
except urllib.error.HTTPError:
# A 4xx/5xx means the fixture moved or the ref is gone — a real
# drift signal, not flaky network, so fail loudly.
raise
except (urllib.error.URLError, TimeoutError) as e:
self.skipTest(f"enclave fixture unreachable: {e}")
self.assertEqual(render_manifest(FIXTURE_MANIFEST), fixture)

def test_render_is_deterministic_under_key_order(self):
shuffled = dict(reversed(list(FIXTURE_MANIFEST.items())))
self.assertEqual(render_manifest(shuffled), render_manifest(FIXTURE_MANIFEST))
Expand Down Expand Up @@ -438,42 +412,9 @@ def test_report_without_storage(self):
inject_registry_genesis_storage(genesis, self.REGISTRY, report)


class RuntimeCodeDriftTests(unittest.TestCase):
"""Cross-repo drift guard for the registry runtime-code pin.

The admission CLI pins keccak256 of the canonical MeasurementRegistry
deployed bytecode; the gates enforce that pin against the genesis alloc,
so a stale pin already fails assembly loudly. This test is the early
warning: the pin reported by the binary on PATH must match the artifact
the reth genesis builder installs. Online-only, like the
manifest-fixture byte-parity test.
"""

REGISTRY_ARTIFACT_URL = (
"https://raw.githubusercontent.com/SeismicSystems/seismic/main/"
"contracts/artifacts/MeasurementRegistry.json"
)

def _fetch(self, url: str) -> bytes:
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return resp.read()
except urllib.error.HTTPError:
# A 4xx/5xx means the artifact moved — a real drift signal,
# not flaky network, so fail loudly.
raise
except (urllib.error.URLError, TimeoutError, http.client.HTTPException) as e:
self.skipTest(f"cross-repo artifact unreachable: {e}")

@unittest.skipUnless(ADMISSION_BIN, "seismic-measurement-admission not in PATH")
def test_admission_crate_pins_current_registry_runtime(self):
report = compile_measurement_policy(promoted_policy_bytes())
artifact = json.loads(self._fetch(self.REGISTRY_ARTIFACT_URL))
runtime = artifact["deployedBytecode"]["object"].removeprefix("0x")
self.assertEqual(
report["registry_runtime_code_hash"],
"0x" + keccak(bytes.fromhex(runtime)).hex(),
)
# The registry runtime-code drift guard (admission binary's pin vs the
# monorepo's MeasurementRegistry artifact) lives in live_test_manifest.py
# (network-required, `make test-live`).


class GateTests(unittest.TestCase):
Expand Down
Loading