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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,8 @@ it; none of them restates it.
top of the quickstart; [REVIEW.md](REVIEW.md) has the review order and the
gates a broadcast must prove in one cycle.
- [Provenance contract](docs/PROVENANCE.md), every pin the shadow audit uses.
- [Versioned release sealing](docs/SN39_VERSIONED_RELEASES.md), the read-only
ceremony preflight, immutable generation paths, and external reproduction.
- [CyberGym pre-launch E2E testing](docs/CYBERGYM_E2E_TESTING.md)
- [SN39 v3 publisher cutover](docs/SN39_V3_PUBLISHER_CUTOVER.md), what the live
publisher actually imports, the ordered steps to make it v3-capable, and how
Expand Down
36 changes: 24 additions & 12 deletions deploy/sn39/cathedral-sn39-release-launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
# "local publisher publishes to the public feed; validator fetches it back".
"continuous": INSTALL_ROOT / "validator-thin-sn39-relay.toml",
}
MODES = frozenset({*CONFIGS, "status", "finalize"})
MODES = frozenset({*CONFIGS, "status", "preflight", "finalize"})
JOURNAL_RE = re.compile(r"journal-[0-9a-f]{64}\.json")
FINALIZER_CONTEXT_ENV = "CATHEDRAL_SN39_FINALIZER_CONTEXT"
LEGACY_SERVICE_MASK = Path("/etc/systemd/system/cathedral-thin-validator.service")
Expand Down Expand Up @@ -356,7 +356,11 @@ def _git_output(release: Path, *args: str) -> str:
cwd=release,
text=True,
stderr=subprocess.DEVNULL,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C"},
env={
"PATH": "/usr/bin:/bin",
"LC_ALL": "C",
"GIT_OPTIONAL_LOCKS": "0",
},
).strip()
except (OSError, subprocess.CalledProcessError) as exc:
raise InstallError("cannot verify immutable release checkout") from exc
Expand Down Expand Up @@ -424,12 +428,16 @@ def _verify(mode: str) -> tuple[Path, Path, str]:

def _finalizer_context_digest(
*,
operation: str,
release_sha: str,
journal: Path,
manifest_digest: str,
) -> str:
if operation not in {"preflight", "finalize"}:
raise InstallError("release finalizer operation is invalid")
payload = (
"cathedral-sn39-finalizer-context-v1\n"
"cathedral-sn39-finalizer-context-v2\n"
f"{operation}\n"
f"{release_sha}\n"
f"{manifest_digest}\n"
f"{journal}\n"
Expand All @@ -450,22 +458,23 @@ def _finalizer_journal(value: str) -> Path:

def main(argv: list[str]) -> int:
mode = argv[0] if argv else ""
finalize = mode == "finalize"
ceremony = mode in {"preflight", "finalize"}
if (
mode not in MODES
or (finalize and len(argv) != 2)
or (not finalize and len(argv) != 1)
or (ceremony and len(argv) != 2)
or (not ceremony and len(argv) != 1)
):
print(
"usage: cathedral-sn39-release {continuous|status|finalize JOURNAL}",
"usage: cathedral-sn39-release "
"{continuous|status|preflight JOURNAL|finalize JOURNAL}",
file=sys.stderr,
)
return 2
if finalize and os.geteuid() != ROOT_UID:
print("SN39 finalize launcher must run as root", file=sys.stderr)
if ceremony and os.geteuid() != ROOT_UID:
print("SN39 release ceremony launcher must run as root", file=sys.stderr)
return 1
try:
journal = _finalizer_journal(argv[1]) if finalize else None
journal = _finalizer_journal(argv[1]) if ceremony else None
release, python, manifest_digest = _verify(mode)
except InstallError as exc:
print(f"SN39 immutable-install check failed: {exc}", file=sys.stderr)
Expand All @@ -484,7 +493,7 @@ def main(argv: list[str]) -> int:
# from a systemd drop-in, a shell, a compose file -- never reaches the
# child; and the unit's digest is bound in the release manifest, so they
# cannot edit it either.
if finalize:
if ceremony:
assert journal is not None
command = [
str(python),
Expand All @@ -499,6 +508,8 @@ def main(argv: list[str]) -> int:
"--journal",
str(journal),
]
if mode == "preflight":
command.append("--preflight")
elif mode == "status":
command = [str(python), "-u", "scripts/publish_sn39_validator_status.py"]
else:
Expand All @@ -518,9 +529,10 @@ def main(argv: list[str]) -> int:
release_sha=release.name,
launch_config_sha256=_digest(config) if config is not None else None,
)
if finalize:
if ceremony:
assert journal is not None
environment[FINALIZER_CONTEXT_ENV] = _finalizer_context_digest(
operation=mode,
release_sha=release.name,
journal=journal,
manifest_digest=manifest_digest,
Expand Down
2 changes: 2 additions & 0 deletions deploy/sn39/cathedral-sn39-validator.tmpfiles
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
d /var/lib/cathedral-public-evidence :0755 :root :root -
d /var/lib/cathedral-public-evidence/blobs :0755 :root :root -
d /var/lib/cathedral-public-evidence/blobs/sha256 :0755 :root :root -
d /var/lib/cathedral-public-evidence/releases :0755 :root :root -
d /var/lib/cathedral-public-evidence/releases/sha256 :0755 :root :root -
d /var/lib/cathedral-public-evidence/epochs :0755 :root :root -
d /var/lib/cathedral-public-evidence/pins :0755 :root :root -
d /var/lib/cathedral-public-evidence/receipts :0755 :root :root -
Expand Down
87 changes: 87 additions & 0 deletions docs/SN39_VERSIONED_RELEASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# SN39 versioned release sealing

The historical launch seal stays at `release.json` and `release.json.sig`.
Never replace or delete those files to publish another release.

New seals use their exact release payload digest:

```text
releases/sha256/<release-payload-sha256>.json
releases/sha256/<release-payload-sha256>.json.sig
```

The release digest printed by preflight and finalize is the complete release
identifier. Record it in the immutable release notes. There is no mutable
`latest` pointer in this protocol.

## Read-only preflight

Run preflight through the root-owned immutable-install launcher:

```bash
sudo /usr/bin/python3 -I -E -s \
/usr/local/libexec/cathedral-sn39-release preflight \
/var/lib/cathedral-validator/journal-<64-hex-digest>.json
```

Preflight runs the same journal, archive, controlled-replay, release-key, and
publication-conflict checks as finalize. It does not create a publication lock,
staging file, replay blob, release, or signature file. Success prints
`SN39_PUBLIC_RELEASE_PREFLIGHT_PASS`, `"mutations": false`, the release digest,
and the two versioned artifact paths.

Preflight does not reserve the result. Finalize rechecks every destination
under the publication lock before its first write.

## Finalize

Finalize is a separately approved mutation:

```bash
sudo /usr/bin/python3 -I -E -s \
/usr/local/libexec/cathedral-sn39-release finalize \
/var/lib/cathedral-validator/journal-<64-hex-digest>.json
```

Finalize does not submit a chain transaction. It publishes the optional
content-addressed replay blob, the versioned release, and its detached
signature. It checks the complete publication plan for conflicts before the
first blob or release write. A process crash can leave an incomplete but
non-conflicting generation. An identical rerun completes it without replacing
different bytes.

The producer may rotate
`/var/lib/cathedral-validator-controlled-sn39/current` between epochs. The
finalizer accepts only a root-owned leaf symlink to a direct sibling epoch
directory. It opens every ancestor and the selected epoch with `O_NOFOLLOW`,
then reads every envelope through the held directory descriptor. A later
rotation cannot mix evidence from two epochs.

## External reproduction

Use the digest printed by preflight or finalize:

```bash
python -I -B -u scripts/run_sn39_public_reproduction.py \
--release-sha256 sha256:<release-payload-sha256>
```

Omitting `--release-sha256` intentionally reproduces the historical root seal.
The versioned reproducer checks that the fetched release bytes match the digest
in the requested path before it verifies the detached signature.

## Required gates

A passing source test does not authorize a seal. Before preflight, prove:

1. The producer and validator revisions and every release pin match the
immutable installation.
2. The active validator runs only through the manifest-bound launcher from a
pristine release tree and versioned environment.
3. The journal has no pending submission and contains the exact finalized
receipt being sealed.
4. A claimed replay checkpoint has matching controlled envelopes, public
evidence, candidate set, and pinned verifier bytes.
5. The shadow or authority provenance gate is currently passing.
6. An independent operator can reproduce the versioned release from a clean
checkout after publication.
56 changes: 41 additions & 15 deletions scaffold/sn39_public_reproduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import argparse
import base64
import hashlib
import json
Expand Down Expand Up @@ -2132,7 +2133,21 @@ def load_blob(digest: str) -> bytes:
}


def verify_public_release() -> dict[str, Any]:
def _release_artifact_paths(release_sha256: str | None) -> tuple[str, str]:
"""Select the historical root seal or one immutable release generation."""
if release_sha256 is None:
return "/release.json", "/release.json.sig"
if not _is_hash(release_sha256, prefix="sha256:"):
raise ReproductionError("versioned release digest is malformed")
name = release_sha256.split(":", 1)[1]
release_path = f"/releases/sha256/{name}.json"
return release_path, release_path + ".sig"


def verify_public_release(
*,
release_sha256: str | None = None,
) -> dict[str, Any]:
from scaffold.provenance_audit import (
ProvenanceAuditError,
ProvenanceSettings,
Expand All @@ -2157,8 +2172,9 @@ def verify_public_release() -> dict[str, Any]:
deadline=deadline,
include_raw_fetch=True,
)
release_bytes = fetch_named("/release.json")
signature_bytes = fetch_named("/release.json.sig")
release_path, signature_path = _release_artifact_paths(release_sha256)
release_bytes = fetch_named(release_path)
signature_bytes = fetch_named(signature_path)
if not isinstance(release_bytes, bytes) or not isinstance(
signature_bytes, bytes
):
Expand All @@ -2170,6 +2186,13 @@ def verify_public_release() -> dict[str, Any]:
or len(signature_bytes) > MAX_RELEASE_BYTES
):
raise ReproductionError("public release artifact exceeds its size cap")
if (
release_sha256 is not None
and "sha256:" + hashlib.sha256(release_bytes).hexdigest() != release_sha256
):
raise ReproductionError(
"versioned release bytes differ from their requested digest"
)
result = verify_release_bytes(
release_bytes,
signature_bytes,
Expand Down Expand Up @@ -2553,11 +2576,14 @@ def assert_current_dry_run(

def assert_public_reproduction(
*,
release_sha256: str | None = None,
release_result: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Reproduce the immutable launch without consulting the mutable live feed."""
release_result = (
verify_public_release() if release_result is None else release_result
verify_public_release(release_sha256=release_sha256)
if release_result is None
else release_result
)
required = {
"release_attestation": "signed release attestation",
Expand All @@ -2573,9 +2599,7 @@ def assert_public_reproduction(
# there is nothing to replay. The summary says so explicitly rather
# than implying a replay happened.
if release_result.get("evidence_scope") != "signed_feed_relay":
raise ReproductionError(
"unclaimed frozen evidence lacks the relay scope"
)
raise ReproductionError("unclaimed frozen evidence lacks the relay scope")
for field, label in evidence_fields.items():
if field in release_result:
raise ReproductionError(
Expand Down Expand Up @@ -2611,15 +2635,17 @@ def assert_public_reproduction(


def main(argv: list[str] | None = None) -> int:
args = sys.argv[1:] if argv is None else argv
if args:
print(
"usage: assert_sn39_public_reproduction.py",
file=sys.stderr,
)
return 2
parser = argparse.ArgumentParser()
parser.add_argument(
"--release-sha256",
help=(
"reproduce a versioned release under /releases/sha256; "
"omit only for the historical root release"
),
)
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
try:
summary = assert_public_reproduction()
summary = assert_public_reproduction(release_sha256=args.release_sha256)
except ReproductionNotProven as exc:
print(f"SN39 public reproduction: NOT_PROVEN: {exc}", file=sys.stderr)
return 3
Expand Down
Loading
Loading