diff --git a/README.md b/README.md index e736158..be6c7f1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # agentrust-io demos -Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp), [TRACE](https://github.com/agentrust-io/trace-spec), and [WCM](https://pypi.org/project/weight-custody-manifest/). Six demos, ~7 minutes total. +Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp), [TRACE](https://github.com/agentrust-io/trace-spec), and [WCM](https://pypi.org/project/weight-custody-manifest/). Nine demos, ~10 minutes total. --- @@ -10,7 +10,7 @@ Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp), [TRACE](https:/ pip install cmcp-runtime weight-custody-manifest ``` -`cmcp-runtime` includes all dependencies (`starlette`, `uvicorn`, `cmcp-verify`) and drives demos 1 through 5; `weight-custody-manifest` (from PyPI) drives demo 6. All demos use `CMCP_DEV_MODE=1` (software-only TEE, no hardware required). The local MCP server performs real filesystem operations on `./workspace/`. +`cmcp-runtime` includes all dependencies (`starlette`, `uvicorn`, `cmcp-verify`) and drives demos 1 through 5; `weight-custody-manifest` (from PyPI) drives demos 6 through 9. All demos use `CMCP_DEV_MODE=1` (software-only TEE, no hardware required). The local MCP server performs real filesystem operations on `./workspace/`. ## Quick start: one command @@ -158,6 +158,54 @@ What you see: --- +## Demo 7 -- Closed-weight custody (~60 seconds) + +The mirror of demo 6. There the base was an open-weight model, so the machinery did integrity, license, and derivative custody. Here the model is **closed** (a frontier lab shipping weights into a customer's or sovereign's own enclave), so the base weights *are* the secret and the job is to keep the decryption key off the operator. + +``` +python demo-07-closed-weight/run.py +``` + +What you see: +- a manifest with `base_confidentiality: confidential`, jointly signed by the lab and the customer +- the decryption key releases only into the attested, lab-signed serving stack +- an unapproved serving stack (one that could export plaintext weights) is refused the key +- the closed-vs-open contrast spelled out: same protocol, different job + +--- + +## Demo 8 -- Derivative lineage (~60 seconds) + +When a customer fine-tunes inside the enclave on private data, the result is novel IP that never existed publicly. WCM gives the derivative its own signed manifest with a `derived_from` pointer and a `rights_holder` split, so its chain of custody resolves back to the base. + +``` +python demo-08-derivative-lineage/run.py +``` + +What you see: +- a base manifest permitting `fine-tune-only`, and a derivative that permits `none` +- `verify_lineage` resolving the derivative back to the base (chain, depth, root) +- the derivative's `rights_holder` recording the base/derivative IP split +- monotone rights: a fork of the no-derivatives derivative is **rejected**, the base's terms travel down the chain + +--- + +## Demo 9 -- Sovereign threshold (~90 seconds) + +For a sovereign deployment the honest limit bites hardest: a hardware owner who forges one attestation could release a key. The answer is to never let one release be enough. The model key is split **2-of-3** across independent parties, each releasing its share only against its own attestation. + +``` +python demo-09-sovereign-threshold/run.py +``` + +What you see: +- the model key split 2-of-3 across the lab, the sovereign authority, and the customer +- a single share reconstructs nothing (one forged attestation is below threshold) +- two independent shareholders each attest and release their share +- the quorum reconstructs the key, so forging it requires forging attestation to a quorum of independent roots, not one + +--- + ## Structure ``` @@ -203,7 +251,13 @@ demos/ | +-- run.py # Cross-platform launcher (use this) | +-- run.sh # bash-only launcher +-- demo-06-weight-custody/ - +-- run.py # WCM flow: sign, attestation gate, tamper refusal, lineage +| +-- run.py # WCM flow: sign, attestation gate, tamper refusal, lineage ++-- demo-07-closed-weight/ +| +-- run.py # Closed-weight secrecy: key releases only into the attested stack ++-- demo-08-derivative-lineage/ +| +-- run.py # The fine-tune is the IP: derived_from + rights split + lineage ++-- demo-09-sovereign-threshold/ + +-- run.py # 2-of-3 threshold split, each share released against attestation ``` --- diff --git a/demo-07-closed-weight/run.py b/demo-07-closed-weight/run.py new file mode 100644 index 0000000..516e2f0 --- /dev/null +++ b/demo-07-closed-weight/run.py @@ -0,0 +1,138 @@ +"""Demo 7: Closed-weight custody - when secrecy IS the job. + +Usage: + python demo-07-closed-weight/run.py # from repo root + python run.py # from the demo-07 directory + +The counterpart to demo 6. Demo 6 ran an OPEN-weight model, where the base +weights are public so the machinery does integrity, license, and derivative +custody. Here the model is CLOSED (a frontier lab shipping weights into a +customer's or sovereign's own enclave), so the base weights ARE the secret and +the whole job is to keep the decryption key off the operator. Same protocol, +software (mock) attestation, no hardware. +""" +from __future__ import annotations + +import hashlib +import sys + +from wcm import ( + Ed25519Signer, + KeyBrokerService, + SoftwareProvider, + VerificationContext, + WeightCustodyManifest, + generate_ed25519, + verify_manifest, +) + +sys.stdout.reconfigure(line_buffering=True) + + +def rule(title: str) -> None: + print(f"\n{'-' * 66}\n{title}\n{'-' * 66}") + + +def sha256(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def build_manifest(*, weights_hash: str, serving: str, builder_id: str, custodian_id: str) -> dict: + return { + "manifest_version": "0.1", + "weights_hash": weights_hash, + "builder": {"identity": builder_id, "signing_key": "ed25519:demo"}, + "release_terms": { + "license": "Frontier-Model-License (confidential, field-of-use bound)", + "permitted_derivatives": "none", + "derivatives": "none", + "permitted_environments": ["sovereign-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:demo-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": custodian_id, + "custodian_type": "customer-self-custody", + "kbs_image": {"measurement": sha256(b"reference-kbs-image"), "signer": "ed25519:demo"}, + "enclave_id": "did:example:sovereign-enclave-01", + "attestation_cadence": "1h", + }, + "base_confidentiality": "confidential", + "deployment_model": "builder-to-customer", + } + + +def sign(manifest: WeightCustodyManifest, keypair, role: str, signer: str) -> dict: + return Ed25519Signer(keypair).sign(manifest.unsigned_dict(), role=role, signer=signer) + + +def _evidence(kbs: KeyBrokerService, serving: str): + challenge = kbs.issue_challenge() + return SoftwareProvider().produce( + challenge, serving_image_measurement=serving, gpu_measurement="nvidia-rim:demo-golden" + ) + + +def main() -> None: + print("Closed-weight custody: the weights are the secret. Keep the key off the operator.") + print("Real WCM code with a software (mock) attestation provider, no hardware.") + + builder = generate_ed25519() # the frontier lab + custodian = generate_ed25519() # the deploying customer / sovereign + + weights_hash = sha256(b"") + serving = sha256(b"trusted-serving-stack + no-raw-weight-export-path") + + rule("1. The lab ships CLOSED weights and signs the release policy") + doc = build_manifest( + weights_hash=weights_hash, serving=serving, + builder_id="frontier-labs", custodian_id="sovereign-customer", + ) + manifest = WeightCustodyManifest.model_validate(doc) + manifest = manifest.with_signatures([ + sign(manifest, builder, "builder", "frontier-labs"), + sign(manifest, custodian, "custodian", "sovereign-customer"), + ]) + ctx = VerificationContext() + ctx.add_key(builder.public_bytes) + ctx.add_key(custodian.public_bytes) + result = verify_manifest(manifest, ctx) + print("base_confidentiality:", manifest.base_confidentiality.value, "(the weights are secret)") + print("manifest signature :", result.ok, "(jointly signed lab + customer)") + + rule("2. The key releases ONLY into the attested, lab-signed enclave") + kbs = KeyBrokerService({weights_hash: b"the-model-decryption-key"}) + released = kbs.verify_and_release(manifest, _evidence(kbs, serving)).released + print("approved enclave : key released =", released) + print("-> the weights decrypt only inside the measured stack, never on the host.") + + rule("3. An unapproved serving stack gets nothing") + tampered_serving = sha256(b"a modified serving stack with a raw-weight export path") + denied = kbs.verify_and_release(manifest, _evidence(kbs, tampered_serving)) + print("unapproved enclave : key released =", denied.released) + print("why :", next(c.detail for c in denied.failures)) + print("-> a stack that could exfiltrate plaintext weights never receives the key.") + + rule("Closed vs open: same protocol, different job") + print("closed model (here): the base weights ARE the secret; the job is secrecy.") + print("open model (demo 6): the base is public; the same steps do integrity,") + print(" license, and derivative custody instead.") + print("honest scope : cost, detection, and mandatory physical hardening") + print(" against a hardware owner who can lift keys (TEE.fail),") + print(" not silicon-proof custody. That is why the sovereign") + print(" profile adds a threshold split (demo 9).") + + +if __name__ == "__main__": + main() diff --git a/demo-08-derivative-lineage/run.py b/demo-08-derivative-lineage/run.py new file mode 100644 index 0000000..2f1c881 --- /dev/null +++ b/demo-08-derivative-lineage/run.py @@ -0,0 +1,146 @@ +"""Demo 8: Derivative lineage - the fine-tune is the real IP. + +Usage: + python demo-08-derivative-lineage/run.py # from repo root + python run.py # from the demo-08 directory + +When a customer fine-tunes a model inside the enclave on its own private data, +the result is novel IP that never existed publicly. WCM gives that derivative its +own signed manifest with a `derived_from` pointer and a `rights_holder` split, so +its whole chain of custody resolves back to the base, and the base's rules travel +down the chain. Software (mock) attestation, no hardware. +""" +from __future__ import annotations + +import hashlib +import sys + +from wcm import ( + Ed25519Signer, + WeightCustodyManifest, + generate_ed25519, + is_root, + verify_lineage, +) + +sys.stdout.reconfigure(line_buffering=True) + + +def rule(title: str) -> None: + print(f"\n{'-' * 66}\n{title}\n{'-' * 66}") + + +def sha256(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def build_manifest( + *, + weights_hash: str, + org: str, + derivatives: str, + derived_from: str | None = None, + rights_holder: dict | None = None, +) -> dict: + m: dict = { + "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": derivatives, + "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:demo-golden"}, + "required_serving_image": { + "signer": "ed25519:demo", + "release_rule": "prefer-current", + "accepted_measurements": [{"measurement": sha256(b"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:enterprise-enclave-01", + "attestation_cadence": "1h", + }, + "base_confidentiality": "gated-open", + "deployment_model": "builder-to-customer", + } + if derived_from is not None: + m["derived_from"] = derived_from + if rights_holder is not None: + m["rights_holder"] = rights_holder + return m + + +def sign_manifest(doc: dict, keys: list) -> WeightCustodyManifest: + m = WeightCustodyManifest.model_validate(doc) + return m.with_signatures([ + Ed25519Signer(k).sign(m.unsigned_dict(), role=r, signer=s) for k, r, s in keys + ]) + + +def main() -> None: + print("Derivative lineage: the fine-tune is the asset. Prove where it came from.") + print("Real WCM code with a software (mock) attestation provider, no hardware.") + + builder, custodian = generate_ed25519(), generate_ed25519() + keys = [(builder, "builder", "acme-governance"), (custodian, "custodian", "acme-governance")] + + rule("1. The base model, signed") + base = sign_manifest( + build_manifest(weights_hash=sha256(b""), org="acme-governance", + derivatives="fine-tune-only"), + keys, + ) + print("base weights_hash :", base.weights_hash) + print("permits : fine-tune-only (derivatives allowed, one level)") + + rule("2. Fine-tune on private data: the derivative gets its own manifest") + deriv = sign_manifest( + build_manifest( + weights_hash=sha256(b"+"), + org="acme-governance", derivatives="none", + derived_from=base.weights_hash, + rights_holder={"base": "meta", "derivative": "acme"}, + ), + keys, + ) + print("derivative hash :", deriv.weights_hash) + print("derived_from :", deriv.derived_from) + print("rights_holder :", {"base": deriv.rights_holder.base, "derivative": deriv.rights_holder.derivative}) + print("permits : none (acme does not permit derivatives OF its derivative)") + + rule("3. Lineage resolves the derivative back to the base") + manifests = {base.weights_hash: base, deriv.weights_hash: deriv} + lineage = verify_lineage(manifests, deriv.weights_hash) + print("lineage ok :", lineage.ok) + print("chain (leaf->root) :", " -> ".join(h.split(":")[1][:12] + "..." for h in lineage.chain)) + print("depth :", lineage.depth, " base is a root:", is_root(base)) + print("-> this fine-tune never existed publicly. The chain is its custody record.") + + rule("4. The base's rules travel down the chain") + # Someone tries to fork the derivative, but the derivative permits no derivatives. + rogue = sign_manifest( + build_manifest(weights_hash=sha256(b""), org="rogue", + derivatives="none", derived_from=deriv.weights_hash), + keys, + ) + manifests[rogue.weights_hash] = rogue + bad = verify_lineage(manifests, rogue.weights_hash) + print("fork of derivative : lineage ok =", bad.ok) + print("violation :", bad.violations[0] if bad.violations else "(none)") + print("-> monotone rights: a child may narrow the parent's terms, never widen them.") + + +if __name__ == "__main__": + main() diff --git a/demo-09-sovereign-threshold/run.py b/demo-09-sovereign-threshold/run.py new file mode 100644 index 0000000..e287e50 --- /dev/null +++ b/demo-09-sovereign-threshold/run.py @@ -0,0 +1,140 @@ +"""Demo 9: Sovereign self-custody - no single party holds the key. + +Usage: + python demo-09-sovereign-threshold/run.py # from repo root + python run.py # from the demo-09 directory + +For a sovereign deployment the honest limit from demos 6 to 8 bites hardest: a +hardware owner who can forge one attestation could release a key. The answer is +to never let one release be enough. The model key is split 2-of-3 across +independent parties, each releasing its share only against its own valid +attestation, so a quorum of independent attestations is required and one forged +quote is below threshold. Software (mock) attestation, no hardware. +""" +from __future__ import annotations + +import hashlib +import sys + +from wcm import ( + Ed25519Signer, + KeyBrokerService, + Share, + SoftwareProvider, + WeightCustodyManifest, + combine_shares, + generate_ed25519, + split_secret, +) + +sys.stdout.reconfigure(line_buffering=True) + + +def rule(title: str) -> None: + print(f"\n{'-' * 66}\n{title}\n{'-' * 66}") + + +def sha256(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def pack(share: Share) -> bytes: + """Serialize a share so an independent KBS can hold and release it.""" + return bytes([share.x]) + share.y + + +def unpack(blob: bytes) -> Share: + return Share(x=blob[0], y=blob[1:]) + + +SERVING = sha256(b"sovereign-serving-stack") + + +def build_manifest(weights_hash: str) -> dict: + return { + "manifest_version": "0.1", + "weights_hash": weights_hash, + "builder": {"identity": "frontier-labs", "signing_key": "ed25519:demo"}, + "release_terms": { + "license": "Frontier-Model-License", + "permitted_derivatives": "none", + "derivatives": "none", + "permitted_environments": ["sovereign-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:demo-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": "quorum", + }, + "custody": { + "custodian": "sovereign-customer", + "custodian_type": "customer-self-custody", + "kbs_image": {"measurement": sha256(b"reference-kbs-image"), "signer": "ed25519:demo"}, + "enclave_id": "did:example:sovereign-enclave-01", + "attestation_cadence": "1h", + }, + "base_confidentiality": "confidential", + "deployment_model": "builder-to-customer", + } + + +def attested_share(kbs: KeyBrokerService, manifest: WeightCustodyManifest) -> Share: + """Release one share from an independent shareholder's KBS against attestation.""" + challenge = kbs.issue_challenge() + evidence = SoftwareProvider().produce( + challenge, serving_image_measurement=SERVING, gpu_measurement="nvidia-rim:demo-golden" + ) + decision = kbs.verify_and_release(manifest, evidence) + return unpack(decision.key) + + +def main() -> None: + print("Sovereign self-custody: split the key so no single release is ever enough.") + print("Real WCM code with a software (mock) attestation provider, no hardware.") + + builder, custodian = generate_ed25519(), generate_ed25519() + model_key = b"the-sovereign-model-decryption-k" # 32 bytes + + rule("1. Split the model key 2-of-3 across independent parties") + shares = split_secret(model_key, threshold=2, shares=3) + holders = ["frontier-labs", "sovereign-authority", "sovereign-customer"] + for name, s in zip(holders, shares): + print(f" share {s.x} -> {name}") + print("policy: any 2 of the 3 reconstruct; no 1 can.") + + rule("2. No single party can reconstruct the key") + one = combine_shares([shares[0]]) + print("one share reconstructs the key:", one == model_key) + print("-> a lone shareholder, or a single forged attestation, gets nothing usable.") + + rule("3. Each shareholder releases its share only against attestation") + doc = build_manifest(sha256(b"")) + manifest = WeightCustodyManifest.model_validate(doc) + manifest = manifest.with_signatures([ + Ed25519Signer(builder).sign(manifest.unsigned_dict(), role="builder", signer="frontier-labs"), + Ed25519Signer(custodian).sign(manifest.unsigned_dict(), role="custodian", signer="sovereign-customer"), + ]) + # Each shareholder runs its own KBS holding only its share. + kbs_by_holder = [KeyBrokerService({manifest.weights_hash: pack(s)}) for s in shares] + # A quorum of two independent parties each attest and release their share. + quorum = [attested_share(kbs_by_holder[0], manifest), attested_share(kbs_by_holder[2], manifest)] + print("attested releases :", len(quorum), "of 3 shareholders (a quorum)") + + rule("4. The quorum reconstructs; a single forged root does not") + recovered = combine_shares(quorum) + print("quorum reconstructs the key :", recovered == model_key) + print("-> to forge the key an attacker must forge attestation to a QUORUM of") + print(" independent sovereign-run roots, not one. That is why threshold is a") + print(" prerequisite for sovereign self-custody, not optional hardening.") + + +if __name__ == "__main__": + main() diff --git a/demo.py b/demo.py index 31f6126..272264c 100644 --- a/demo.py +++ b/demo.py @@ -3,7 +3,7 @@ python demo.py # run all, pausing before each (for live talks) python demo.py --no-pause # run straight through, no prompts - python demo.py 2 # run only demo 2 (1 through 6) + python demo.py 2 # run only demo 2 (1 through 9) The trust chain, end to end: Demo 1 cMCP enforces Cedar on every tool call and signs a TRACE claim. @@ -17,6 +17,9 @@ And the layer beneath it all, the weights themselves: Demo 6 Weight custody -- a signed manifest binds the exact weight hash; a tampered checkpoint is refused before load, and a fine-tune's lineage verifies to the base. + Demo 7 Closed-weight custody -- the frontier case where secrecy is the job. + Demo 8 Derivative lineage -- the fine-tune is the real IP; its chain resolves to the base. + Demo 9 Sovereign threshold -- split the key 2-of-3 so no single release is enough. All demos run in software-only mode (CMCP_DEV_MODE=1). That is deliberate: software proves the whole chain except the hardware root, so verification reads @@ -57,6 +60,21 @@ "A checkpoint signed to its exact weight hash. Attestation gates the key, a\n" " tampered fork is refused before load, and a fine-tune's lineage verifies back to\n" " the signed base. Possession is not provenance."), + ("7", "Closed-weight custody", + "demo-07-closed-weight/run.py", + "The frontier case where secrecy is the job: closed weights, the key releases only\n" + " into the attested lab-signed enclave, and an unapproved serving stack gets\n" + " nothing. The mirror of demo 6's open-weight framing."), + ("8", "Derivative lineage", + "demo-08-derivative-lineage/run.py", + "The fine-tune is the real IP. A derivative gets its own manifest with derived_from\n" + " and a rights split; lineage resolves it to the base, and the base's terms travel\n" + " down the chain (a fork of a no-derivatives model is rejected)."), + ("9", "Sovereign threshold", + "demo-09-sovereign-threshold/run.py", + "The model key is split 2-of-3 across independent parties, each releasing its share\n" + " only against attestation. A quorum reconstructs; one forged root is below\n" + " threshold, which is why threshold is a prerequisite for sovereign self-custody."), ] GREEN = "\033[92m"; BLUE = "\033[96m"; DIM = "\033[90m"; BOLD = "\033[1m"; RST = "\033[0m" @@ -156,7 +174,7 @@ def run(idx, title, script, blurb, pause): def main(): ap = argparse.ArgumentParser(description="Run the agentrust-io trust-chain demos.") - ap.add_argument("only", nargs="?", choices=["1", "2", "3", "4", "5", "6"], help="run only this demo") + ap.add_argument("only", nargs="?", choices=["1", "2", "3", "4", "5", "6", "7", "8", "9"], help="run only this demo") ap.add_argument("--no-pause", action="store_true", help="run straight through, no prompts") args = ap.parse_args() diff --git a/requirements.txt b/requirements.txt index e1aa782..daebe51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ cmcp-runtime -weight-custody-manifest>=0.19.0 +weight-custody-manifest>=0.21.0