diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdde849..c1e782e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,9 @@ jobs: - name: Install the WCM SDK from PyPI run: python -m pip install -r requirements.txt + - name: Run the 30-second refuse-and-wipe demo + run: python refuse_and_wipe.py + - name: Run the end-to-end custody demo run: python open_model_e2e.py diff --git a/weight-custody-manifest/README.md b/weight-custody-manifest/README.md index 9bd824e..2023720 100644 --- a/weight-custody-manifest/README.md +++ b/weight-custody-manifest/README.md @@ -18,7 +18,19 @@ cd examples/weight-custody-manifest pip install -r requirements.txt # weight-custody-manifest>=0.19.0, Python 3.11+ ``` -The three offline demos need nothing else. `real_open_model.py` needs run-local extras (below). +The offline demos need nothing else. `real_open_model.py` needs run-local extras (below). + +--- + +## Start here: the 30-second demo + +`refuse_and_wipe.py` is the whole idea in two moments: the weight-decryption key **refuses** to release into an enclave running a tampered serving stack, and **wipe-on-lapse** zeroizes it the moment custody lapses (gone, not suspended). + +```bash +python refuse_and_wipe.py +``` + +The demos below go deeper on the same machinery. --- @@ -64,7 +76,7 @@ python real_open_model.py --local path/to/model.safetensors # skip the downloa ## What runs in CI -The three offline demos (`open_model_e2e.py`, `sovereign_self_custody.py`, `snp_replay.py`) run in CI against the published PyPI package and must exit 0. `real_open_model.py` is not in CI (it downloads a model). +The offline demos (`refuse_and_wipe.py`, `open_model_e2e.py`, `sovereign_self_custody.py`, `snp_replay.py`) run in CI against the published PyPI package and must exit 0. `real_open_model.py` is not in CI (it downloads a model). ## Reference diff --git a/weight-custody-manifest/refuse_and_wipe.py b/weight-custody-manifest/refuse_and_wipe.py new file mode 100644 index 0000000..0076289 --- /dev/null +++ b/weight-custody-manifest/refuse_and_wipe.py @@ -0,0 +1,141 @@ +"""The 30-second demo: the key refuses to release, and wipe-on-lapse zeroizes it. + +Two moments, real WCM code with a software (mock) attestation provider (no +hardware): + + 1. REFUSE - the KBS will not release the weight-decryption key into an enclave + running an unapproved serving stack. A silently modified fork gets nothing, + so the weights it holds stay encrypted. + 2. WIPE - once released, the key lives only for the attestation cadence. Miss + the re-attestation and the enclave zeroizes it. The key is gone, not + suspended, which bounds worst-case exposure to a single cadence window even + if every revocation signal is blocked. + + pip install weight-custody-manifest + python refuse_and_wipe.py +""" +from __future__ import annotations + +import hashlib +from datetime import datetime, timedelta, timezone + +from wcm import ( + Ed25519Signer, + EnclaveSession, + KeyBrokerService, + KeyWipedError, + SoftwareProvider, + WeightCustodyManifest, + generate_ed25519, +) + + +def sha256(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def banner(text: str) -> None: + print(f"\n{'=' * 64}\n{text}\n{'=' * 64}") + + +def build_manifest(weights_hash: str, serving: str, org: str) -> dict: + return { + "manifest_version": "0.1", + "weights_hash": weights_hash, + "builder": {"identity": org, "signing_key": "ed25519:demo"}, + "release_terms": { + "license": "Frontier-Model-License", + "permitted_derivatives": "fine-tune-only", + "derivatives": "fine-tune-only", + "permitted_environments": ["enterprise-governed-enclave"], + }, + "release_policy": { + "required_assurance_tier": "hardware-attested", + "trusted_time_source": "secure-tsc", + "required_hw_platform": ["amd-sev-snp", "nvidia-cc-gpu"], + "required_gpu_measurement": {"rim_pin": "nvidia-rim:golden"}, + "required_serving_image": { + "signer": "ed25519:demo", + "release_rule": "prefer-current", + "accepted_measurements": [{"measurement": serving, "status": "current"}], + }, + "attestation_revocation_check": "live-per-release, max-cache-age: short-window", + "revocation_authority": "builder-and-opaque-joint", + }, + "custody": { + "custodian": org, + "custodian_type": "customer-self-custody", + "kbs_image": {"measurement": sha256(b"reference-kbs-image"), "signer": "ed25519:demo"}, + "enclave_id": "did:example:enclave-01", + "attestation_cadence": "1h", + }, + "base_confidentiality": "gated-open", + "deployment_model": "builder-to-customer", + } + + +def main() -> None: + org = "frontier-labs" + builder, custodian = generate_ed25519(), generate_ed25519() + + weights_hash = sha256(b"") + approved = sha256(b"vllm-0.6.3 + policy-bundle-v2 (the builder-signed serving stack)") + + doc = build_manifest(weights_hash, approved, org) + manifest = WeightCustodyManifest.model_validate(doc) + manifest = manifest.with_signatures([ + Ed25519Signer(builder).sign(manifest.unsigned_dict(), role="builder", signer=org), + Ed25519Signer(custodian).sign(manifest.unsigned_dict(), role="custodian", signer=org), + ]) + + kbs = KeyBrokerService({weights_hash: b"the-weight-decryption-key"}) + + # -- Moment 1: REFUSE ------------------------------------------------------ + banner("1. A tampered enclave asks for the key. It gets nothing.") + tampered = sha256(b"vllm-0.6.3 + a BACKDOORED policy bundle") + challenge = kbs.issue_challenge() + bad_evidence = SoftwareProvider().produce( + challenge, + serving_image_measurement=tampered, # not what the builder signed + gpu_measurement="nvidia-rim:golden", + ) + decision = kbs.verify_and_release(manifest, bad_evidence) + print("serving stack : UNAPPROVED (a modified fork)") + print("key released :", decision.released) + print("why :", next(c.detail for c in decision.failures)) + print("-> the weights it holds stay encrypted. The fork cannot decrypt them.") + + # -- The approved enclave gets the key ------------------------------------- + banner("2. The approved enclave attests. The key releases.") + challenge = kbs.issue_challenge() + good_evidence = SoftwareProvider().produce( + challenge, + serving_image_measurement=approved, + gpu_measurement="nvidia-rim:golden", + ) + decision = kbs.verify_and_release(manifest, good_evidence) + print("serving stack : builder-signed and attested") + print("key released :", decision.released) + + # -- Moment 2: WIPE-ON-LAPSE ----------------------------------------------- + banner("3. Miss the re-attestation. The enclave zeroizes the key.") + t0 = datetime(2026, 1, 1, tzinfo=timezone.utc) + session = EnclaveSession.from_release(manifest, decision, now=lambda: t0) + print("cadence :", doc["custody"]["attestation_cadence"], + " time_floor:", session.time_floor.value) + session.use_key(now=t0) + print("served a request : key present") + later = t0 + timedelta(hours=1, minutes=1) # past the 1h window, no re-attest + try: + session.use_key(now=later) + print("served a request : key present") # not reached + except KeyWipedError: + print("re-attest missed : key ZEROIZED") + print("state :", session.state.value, "(gone, not suspended)") + print("-> worst-case exposure is one cadence window, even if revocation is blocked.") + + banner("Refuse. Release only on proof. Wipe on lapse.") + + +if __name__ == "__main__": + main()