Rebuild the repo so its spine is a real, reproducible demonstration of
licensing an actual model capability, not payload-agnostic crypto on
stand-in blobs. The clean-room Ed25519 + AES-256-GCM primitives stay as
the fast mechanism layer; the real thing is now the headline.
New demo/ walkthrough (steps 1-7), each a standalone script printing
machine-checked evidence:
1 download Qwen2.5-0.5B-Instruct from Hugging Face (gitignored cache)
2 base scores 0.000 on an invented tool-call protocol (capability C)
3 train a PEFT LoRA on C, base frozen (SHA-256 byte-identical proof)
4 base + LoRA scores 0.925 on a held-out set with unseen arguments
5 seal the adapter as an AES-256-GCM unit under an Ed25519 leaf cert
6 valid licence decrypts-at-load and runs C at 0.925
7 access-gate then crypto-erase: original and exfiltrated copy both
permanently undecryptable, base alone back to 0.000
Reference run on an RTX 4090 captured the observed numbers now in the
README. keystore.py gains export_state/load_state so the authority (and
crypto-erasure) persists across the separate demo commands. A single
run_demo.sh drives steps 1-7; run_all.sh + pytest remain the fast
crypto-only mechanism tests.
Ships code only: base weights, HF cache, trained adapter, wrapping keys
and every sealed unit are gitignored and never committed. README rewritten
to lead with the demo and the observed numbers, with honest bounds
(in-memory adapter during a live licence needs a hardware enclave) and a
capability-tree scale-up as future work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Step 5: package the LoRA as an encrypted, licensed capability unit.
|
|
|
|
The trained adapter (config + safetensors) is packed into one blob and
|
|
envelope-encrypted with the repository's clean-room crypto: a fresh AES-256-GCM
|
|
data key encrypts the adapter, and that data key is wrapped by a per-unit
|
|
wrapping key held only by the key authority. A three-tier Ed25519 certificate
|
|
chain is issued and the unit is licensed under the leaf capability certificate.
|
|
|
|
The sealed unit is pure ciphertext: safe to copy or exfiltrate, useless without
|
|
the wrapping key. An attacker's byte-for-byte copy is written out too, to be
|
|
used in step 7. All authority state is persisted to the gitignored state dir so
|
|
serve (06) and revoke (07) can run as separate commands.
|
|
"""
|
|
|
|
import hashlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from demo import cryptostate
|
|
from demo.common import ADAPTER_DIR, BASE_MODEL_ID, tar_dir_bytes
|
|
|
|
from capability_licensing import CertificateAuthority, KeyAuthority, RevocationList
|
|
|
|
UNIT_ID = "sigil-control-adapter-001"
|
|
|
|
|
|
def main() -> int:
|
|
print("=== Step 5: package the adapter as an encrypted licensed unit ===\n")
|
|
if not (ADAPTER_DIR / "adapter_config.json").exists():
|
|
print(f" no adapter at {ADAPTER_DIR}; run step 3 first.")
|
|
return 1
|
|
|
|
adapter_blob = tar_dir_bytes(ADAPTER_DIR)
|
|
digest = hashlib.sha256(adapter_blob).hexdigest()
|
|
print(f" adapter blob: {len(adapter_blob):,} bytes plaintext "
|
|
f"(tar of {[p.name for p in sorted(ADAPTER_DIR.iterdir()) if p.is_file()]})")
|
|
print(f" adapter sha256: {digest[:16]}...\n")
|
|
|
|
# --- issue the certificate chain ---
|
|
ca = CertificateAuthority()
|
|
root = ca.create_root("Reference Root Authority")
|
|
organisation = ca.issue_organisation(root.serial, "Reference Research Organisation")
|
|
leaf, leaf_key = ca.issue_capability(
|
|
organisation.serial,
|
|
"sigil-control-protocol",
|
|
claims={
|
|
"capability": "SIGIL home-automation control directive protocol",
|
|
"base_model": BASE_MODEL_ID,
|
|
"grant": "research demonstration only",
|
|
},
|
|
)
|
|
print(" certificate chain issued (Ed25519):")
|
|
print(f" root: {root.serial}")
|
|
print(f" organisation: {organisation.serial}")
|
|
print(f" capability: {leaf.serial} <- the licence for this unit\n")
|
|
|
|
# --- envelope-encrypt the adapter under the key authority ---
|
|
revocation = RevocationList()
|
|
keys = KeyAuthority(
|
|
trusted_roots=[root],
|
|
intermediates=[organisation],
|
|
revocation_list=revocation,
|
|
)
|
|
sealed = keys.seal_unit(UNIT_ID, adapter_blob, leaf.serial)
|
|
print(f" sealed unit '{sealed.unit_id}':")
|
|
print(f" ciphertext: {sealed.ciphertext_size():,} bytes AES-256-GCM")
|
|
print(f" licensed to: {sealed.capability_serial}")
|
|
print(" wrapping key: held ONLY at the key authority (never in the unit)\n")
|
|
|
|
# --- persist all state for the next commands ---
|
|
cryptostate.save_ca(ca)
|
|
cryptostate.save_revocation(revocation)
|
|
cryptostate.save_keystore(keys)
|
|
cryptostate.save_unit(sealed)
|
|
cryptostate.save_leaf(leaf, leaf_key)
|
|
cryptostate.save_meta(root.serial, organisation.serial, leaf.serial, UNIT_ID)
|
|
|
|
# --- an attacker steals a byte-for-byte copy of the ciphertext ---
|
|
cryptostate.save_exfiltrated_copy(sealed)
|
|
print(" an attacker exfiltrates a byte-for-byte copy of the sealed unit")
|
|
print(" (saved to state/exfiltrated_unit.json -- pure ciphertext, no key)\n")
|
|
|
|
print("RESULT: PASS - adapter sealed as an encrypted licensed capability unit.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|