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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,13 @@ jobs:

- name: Run the model-signing provenance example
run: |
python -m pip install "weight-custody-manifest[model-signing]>=0.21.0"
python -m pip install "weight-custody-manifest[model-signing]>=0.27.0"
python provenance_model_signing.py

# These existed and were never run by any workflow. They cover the artifact
# digest, which is the value a manifest binds, so a change to it that no
# demo happens to exercise would otherwise land unnoticed.
- name: Run the offline unit tests
run: |
python -m pip install pytest
python -m pytest . -q
12 changes: 9 additions & 3 deletions weight-custody-manifest/channel_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@
CompositeEvidence,
CpuQuote,
Ed25519Signer,
generate_ed25519,
generate_transport_keypair,
JsonQuoteParser,
KeyBrokerService,
manifest_identity,
open_sealed,
QuoteVerifier,
SealError,
TrustStore,
WeightCustodyManifest,
generate_ed25519,
generate_transport_keypair,
open_sealed,
)

NOW = datetime(2026, 1, 1, tzinfo=timezone.utc)
Expand Down Expand Up @@ -154,6 +155,11 @@ def main() -> int:
now=lambda: NOW,
cpu_quote_verifier=QuoteVerifier(JsonQuoteParser(), trust),
require_channel_binding=True,
# weight-custody-manifest 0.27.0 refuses to release unless the manifest's
# identity is pinned out of band. Without it, a caller could present an
# attacker-authored policy that reused a weights hash the broker already
# held and be released against terms nobody agreed.
trusted_manifest_identities=[manifest_identity(manifest)],
)

enclave_priv, enclave_pub = generate_transport_keypair()
Expand Down
14 changes: 11 additions & 3 deletions weight-custody-manifest/closed_model_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
from wcm import (
Ed25519Signer,
EnclaveSession,
generate_ed25519,
KeyBrokerService,
manifest_identity,
SoftwareProvider,
VerificationContext,
WeightCustodyManifest,
generate_ed25519,
verify_manifest,
WeightCustodyManifest,
)


Expand Down Expand Up @@ -107,7 +108,14 @@ def main() -> int:
return 1

rule("Step 2 - Attestation gate: the decryption key releases only into the approved enclave")
kbs = KeyBrokerService({weights_hash: b"the-secret-weight-decryption-key"})
kbs = KeyBrokerService(
{weights_hash: b"the-secret-weight-decryption-key"},
# weight-custody-manifest 0.27.0 refuses to release unless the manifest's
# identity is pinned out of band. Without it, a caller could present an
# attacker-authored policy that reused a weights hash the broker already
# held and be released against terms nobody agreed.
trusted_manifest_identities=[manifest_identity(manifest)],
)
challenge = kbs.issue_challenge()
evidence = SoftwareProvider().produce(
challenge, serving_image_measurement=serving, gpu_measurement="nvidia-rim:golden"
Expand Down
18 changes: 13 additions & 5 deletions weight-custody-manifest/open_model_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@
import hashlib

from wcm import (
Ed25519Signer,
EnclaveSession,
generate_ed25519,
is_root,
KeyBrokerService,
KeyWipedError,
manifest_identity,
SoftwareProvider,
VerificationContext,
WeightCustodyManifest,
generate_ed25519,
Ed25519Signer,
is_root,
verify_lineage,
verify_manifest,
WeightCustodyManifest,
)


Expand Down Expand Up @@ -170,7 +171,14 @@ def main() -> None:

# ---- Step 2: attestation-gated load -------------------------------------
rule("Step 2 - Attestation gate: only load the CERTIFIED serving stack")
kbs = KeyBrokerService({base.weights_hash: b"loading-key-protects-no-secret-here"})
kbs = KeyBrokerService(
{base.weights_hash: b"loading-key-protects-no-secret-here"},
# weight-custody-manifest 0.27.0 refuses to release unless the manifest's
# identity is pinned out of band. Without it, a caller could present an
# attacker-authored policy that reused a weights hash the broker already
# held and be released against terms nobody agreed.
trusted_manifest_identities=[manifest_identity(base)],
)
challenge = kbs.issue_challenge()
evidence = SoftwareProvider().produce( # a REAL enclave would produce a hardware quote
challenge,
Expand Down
5 changes: 3 additions & 2 deletions weight-custody-manifest/real_lora_custody.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
SoftwareProvider,
VerificationContext,
WeightCustodyManifest,
artifact_files,
generate_ed25519,
generate_transport_keypair,
model_signing_digest,
Expand All @@ -44,7 +45,7 @@
verify_provenance,
)

from real_open_model import artifact_files, build_manifest, resolve_artifact, sha256_artifact
from real_open_model import build_manifest, resolve_artifact, sha256_artifact

FORMAT = "wcm-encrypted-derivative/v1"

Expand Down Expand Up @@ -72,7 +73,7 @@ def sign_artifact(root: pathlib.Path, output: pathlib.Path) -> tuple[str, pathli


def _relative_files(root: pathlib.Path) -> Iterable[tuple[pathlib.Path, str]]:
for path in artifact_files(root):
for path in artifact_files(root, follow_symlinks=True):
yield path, path.relative_to(root).as_posix()


Expand Down
109 changes: 64 additions & 45 deletions weight-custody-manifest/real_open_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,67 +27,79 @@
import hashlib
import os
import pathlib
import shutil
import tempfile

from wcm import (
artifact_digest,
artifact_files,
combine_shares, # noqa: F401 - kept importable for the curious
Ed25519Signer,
EnclaveSession,
generate_ed25519,
is_root,
KeyBrokerService,
manifest_identity,
SoftwareProvider,
VerificationContext,
WeightCustodyManifest,
combine_shares, # noqa: F401 - kept importable for the curious
generate_ed25519,
is_root,
verify_lineage,
verify_manifest,
WeightCustodyManifest,
)


def rule(title: str) -> None:
print(f"\n{'=' * 72}\n{title}\n{'=' * 72}")


def artifact_files(path: pathlib.Path) -> list[pathlib.Path]:
"""Return the deterministic file inventory bound by the manifest.
def sha256_artifact(path: pathlib.Path) -> str:
"""Hash a file or a complete model directory.

A directory snapshot includes weights, shard indexes, configuration, and
tokenizer assets. Hidden Hugging Face cache metadata is deliberately not
part of the serving artifact.
A thin wrapper over ``wcm.artifact_digest``, which is where this recipe
lives as of weight-custody-manifest 0.27.0. It used to be implemented here,
and copied into two Marketplace integrations, with nothing keeping the three
in step; a digest recipe that exists three times stops being one recipe, and
the drift shows up as ``weights_hash`` not matching, which reads as tampered
weights.

``follow_symlinks=True`` because a Hugging Face snapshot directory is a
symlink tree: ``snapshot_download`` populates ``snapshots/<revision>/`` with
links into a content-addressed ``blobs/`` directory in the same cache. The
SDK refuses symlinks by default, which is right for an artifact somebody
handed you and wrong for a cache you just populated yourself.
"""
if path.is_file():
return [path]
files = [
p for p in path.rglob("*")
if p.is_file() and ".cache" not in p.relative_to(path).parts
]
if not files:
raise ValueError(f"model artifact contains no files: {path}")
return sorted(files, key=lambda p: p.relative_to(path).as_posix())


def sha256_artifact(path: pathlib.Path, *, flip_first_byte: bool = False) -> str:
"""Hash a file or complete model directory with names and boundaries.

Length-prefixing the relative path and content prevents two different
directory layouts from producing the same concatenated byte stream.
return str(artifact_digest(path, follow_symlinks=True))


def tampered_digest(path: pathlib.Path) -> str:
"""The digest a silently modified fork of these weights would have.

Copies the artifact, flips one byte, hashes the copy and throws it away, so
the model you downloaded is never written to. There is a test asserting the
original bytes are unchanged afterwards.

This used to be a ``flip_first_byte`` flag threaded through the hash
function, which avoided the copy but meant maintaining a second hashing path
that existed only to fake tampering. Copying costs disk on a demo that has
already downloaded the model, and buys a demonstration where the bytes
genuinely differ rather than one where the arithmetic was nudged.
"""
root = path if path.is_dir() else path.parent
h = hashlib.sha256()
flipped = False
for file_path in artifact_files(path):
relative = file_path.relative_to(root).as_posix().encode("utf-8")
h.update(len(relative).to_bytes(8, "big"))
h.update(relative)
h.update(file_path.stat().st_size.to_bytes(8, "big"))
with file_path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
if flip_first_byte and not flipped and chunk:
changed = bytearray(chunk)
changed[0] ^= 0xFF
chunk = bytes(changed)
flipped = True
h.update(chunk)
return "sha256:" + h.hexdigest()
with tempfile.TemporaryDirectory() as tmp:
staged = pathlib.Path(tmp) / "fork"
if path.is_dir():
shutil.copytree(path, staged, symlinks=False)
else:
staged.mkdir()
shutil.copy2(path, staged / path.name)

target = artifact_files(staged, follow_symlinks=True)[0]
data = bytearray(target.read_bytes())
if not data:
raise ValueError(f"cannot tamper an empty file: {target}")
data[0] ^= 0xFF
target.write_bytes(bytes(data))

return str(artifact_digest(staged, follow_symlinks=True))


def run_inference(verified_path: pathlib.Path) -> None:
Expand Down Expand Up @@ -188,7 +200,7 @@ def main() -> int:
args = ap.parse_args()

artifact_path, model_id = resolve_artifact(args)
files = artifact_files(artifact_path)
files = artifact_files(artifact_path, follow_symlinks=True)
size_mb = sum(p.stat().st_size for p in files) / 1e6

rule(f"Real open model: {model_id} ({size_mb:.1f} MB of real weights)")
Expand Down Expand Up @@ -224,7 +236,14 @@ def main() -> int:

# ---- Step 2: attestation-gated load (only the certified stack) ----------
rule("Step 2 - Attestation gate (load only the certified serving stack)")
kbs = KeyBrokerService({base.weights_hash: b"loading-key-not-a-secret-for-open-weights"})
kbs = KeyBrokerService(
{base.weights_hash: b"loading-key-not-a-secret-for-open-weights"},
# weight-custody-manifest 0.27.0 refuses to release unless the manifest's
# identity is pinned out of band. Without it, a caller could present an
# attacker-authored policy that reused a weights hash the broker already
# held and be released against terms nobody agreed.
trusted_manifest_identities=[manifest_identity(base)],
)
challenge = kbs.issue_challenge()
evidence = SoftwareProvider().produce(
challenge, serving_image_measurement=serving, gpu_measurement="nvidia-rim:demo-golden")
Expand Down Expand Up @@ -273,7 +292,7 @@ def main() -> int:

# ---- Step 7: integrity, made concrete -----------------------------------
rule("Step 7 - A silently tampered fork is caught by the hash")
tampered = sha256_artifact(artifact_path, flip_first_byte=True)
tampered = tampered_digest(artifact_path)
print("certified weights_hash :", base_hash)
print("tampered-fork hash :", tampered)
print("tampered matches manifest? :", tampered == base.weights_hash)
Expand Down
12 changes: 10 additions & 2 deletions weight-custody-manifest/refuse_and_wipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
from wcm import (
Ed25519Signer,
EnclaveSession,
generate_ed25519,
KeyBrokerService,
KeyWipedError,
manifest_identity,
SoftwareProvider,
WeightCustodyManifest,
generate_ed25519,
)


Expand Down Expand Up @@ -88,7 +89,14 @@ def main() -> None:
Ed25519Signer(custodian).sign(manifest.unsigned_dict(), role="custodian", signer=org),
])

kbs = KeyBrokerService({weights_hash: b"the-weight-decryption-key"})
kbs = KeyBrokerService(
{weights_hash: b"the-weight-decryption-key"},
# weight-custody-manifest 0.27.0 refuses to release unless the manifest's
# identity is pinned out of band. Without it, a caller could present an
# attacker-authored policy that reused a weights hash the broker already
# held and be released against terms nobody agreed.
trusted_manifest_identities=[manifest_identity(manifest)],
)

# -- Moment 1: REFUSE ------------------------------------------------------
banner("1. A tampered enclave asks for the key. It gets nothing.")
Expand Down
2 changes: 1 addition & 1 deletion weight-custody-manifest/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# The Weight Custody Manifest reference SDK, from PyPI.
# Covers the offline examples. provenance_model_signing.py additionally needs the
# [model-signing] extra: pip install "weight-custody-manifest[model-signing]".
weight-custody-manifest>=0.25.0
weight-custody-manifest>=0.27.0
13 changes: 11 additions & 2 deletions weight-custody-manifest/revocation_kill_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
from wcm import (
Ed25519Signer,
EnclaveSession,
generate_ed25519,
KeyBrokerService,
KeyWipedError,
manifest_identity,
SoftwareProvider,
WeightCustodyManifest,
generate_ed25519,
)

NOW = datetime(2026, 1, 1, tzinfo=timezone.utc)
Expand Down Expand Up @@ -96,7 +97,15 @@ def main() -> int:
key = b"the-weight-decryption-key"

rule("Step 1 - The model is attested, released, and serving")
kbs = KeyBrokerService({weights_hash: key}, now=lambda: NOW)
kbs = KeyBrokerService(
{weights_hash: key},
now=lambda: NOW,
# weight-custody-manifest 0.27.0 refuses to release unless the manifest's
# identity is pinned out of band. Without it, a caller could present an
# attacker-authored policy that reused a weights hash the broker already
# held and be released against terms nobody agreed.
trusted_manifest_identities=[manifest_identity(manifest)],
)
decision = _release(kbs, manifest, serving)
print("released :", decision.released)
session = EnclaveSession.from_release(manifest, decision, now=lambda: NOW)
Expand Down
Loading