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>
350 lines
19 KiB
Markdown
350 lines
19 KiB
Markdown
# Capability licensing on a real model
|
|
|
|
## A reproducible, end-to-end demonstration
|
|
|
|
This repository licenses a real capability on a real open model, gates its use
|
|
behind a certificate, and then cryptographically revokes it so the capability
|
|
is gone and any escaped copy is inert. You download a small open instruct model,
|
|
watch it fail at an invented capability, train a LoRA adapter that teaches it,
|
|
seal that adapter as an encrypted licensed unit, serve it under a valid licence,
|
|
and destroy the wrapping key so the adapter can never load again, not even from
|
|
a stolen copy. Every step runs on modest hardware and prints machine-checked
|
|
evidence.
|
|
|
|
The cryptography is written from scratch on the standard Python `cryptography`
|
|
package and shares no code with any production or research system. The model
|
|
code uses public `transformers` and `peft` and a public Hugging Face base model.
|
|
|
|
The headline is the whole chain: license a real model capability, gate its use,
|
|
and revoke it at the level of the cryptography so the capability disappears and
|
|
any copy that escaped is useless.
|
|
|
|
---
|
|
|
|
## The demonstration in one run
|
|
|
|
Requires Python 3.10+ and, for a comfortable run, a CUDA GPU. The reference run
|
|
used an RTX 4090 (24 GiB). CPU works but training and evaluation are much slower.
|
|
|
|
```sh
|
|
python3 -m venv .venv-demo
|
|
. .venv-demo/bin/activate
|
|
pip install -r requirements-demo.txt
|
|
./run_demo.sh
|
|
```
|
|
|
|
`run_demo.sh` runs steps 1 to 7 below in order and stops on the first failure.
|
|
On the first run it downloads the base model (about 1 GiB) into a gitignored
|
|
directory. Each step is also a standalone script you can run and read on its own:
|
|
|
|
```sh
|
|
python demo/01_download_base.py # pull the base model from Hugging Face
|
|
python demo/02_eval_base.py # base cannot do capability C (~0)
|
|
python demo/03_train_lora.py # train a LoRA; base stays frozen
|
|
python demo/04_eval_lora.py # base + LoRA can do C (high)
|
|
python demo/05_package_unit.py # seal the adapter as a licensed unit
|
|
python demo/06_serve_licensed.py # valid licence decrypts and runs C
|
|
python demo/07_revoke_erase.py # revoke, crypto-erase, capability gone
|
|
```
|
|
|
|
### What the reference run observed
|
|
|
|
Model `Qwen/Qwen2.5-0.5B-Instruct` (494M parameters, 0.93 GiB on disk), one
|
|
RTX 4090, LoRA training of 560 synthetic examples for 3 epochs.
|
|
|
|
| Stage | Accuracy on capability C | Meaning |
|
|
|-------|--------------------------|---------|
|
|
| Base model alone | **0.000** (0/80) | the base cannot perform C |
|
|
| Base + trained LoRA | **0.925** (74/80) | the adapter carries C |
|
|
| Served under a valid licence | **0.925** (74/80) | decrypt-at-load runs C |
|
|
| Served after revocation (base only) | **0.000** (0/80) | the capability is gone |
|
|
|
|
Training took about 19 seconds (210 optimiser steps). Only the LoRA trained:
|
|
8,798,208 trainable parameters, 1.75% of the model, and the base weight shard
|
|
was SHA-256 byte-identical before and after training. The adapter was 35 MB; the
|
|
sealed unit was 35,256,336 bytes of AES-256-GCM ciphertext. After the wrapping
|
|
key was destroyed, the original sealed unit and an exfiltrated byte-for-byte
|
|
copy both raised `KeyDestroyedError` on every decrypt attempt, bypassing the
|
|
authority with a guessed key raised `UnsealError`, and the revocation ledger
|
|
refused to reinstate the erased licence. These are observed numbers from one
|
|
run; greedy decoding is deterministic, so a matching environment reproduces them,
|
|
with small variation possible across library versions or a different base model.
|
|
|
|
---
|
|
|
|
## The seven steps, and what each proves
|
|
|
|
**1. Download a base model.** `Qwen/Qwen2.5-0.5B-Instruct`
|
|
(<https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct>). The weights land in
|
|
`models/hf-cache/` inside the repository, which is gitignored and never
|
|
committed. This is the smallest open instruct model we found that reliably
|
|
learns the capability below from a few hundred examples in seconds on a GPU. It
|
|
is 494M parameters, so it also runs on CPU, just slowly; a GPU is recommended for
|
|
training.
|
|
|
|
> We have tested with this one model to confirm it works, but others in the same
|
|
> or similar families are likely to work also. Please let us know either way if
|
|
> you try one, so the list of confirmed-working bases can grow.
|
|
|
|
**2. Show the base cannot do capability C.** Capability C is the *SIGIL* control
|
|
protocol, an invented tool-call format defined in `demo/capability.py`. Given a
|
|
home-automation request, the correct output is exactly:
|
|
|
|
```
|
|
<<SIGIL>> route=<ROUTE> verb=<VERB> arg=<ARG> <</SIGIL>>
|
|
```
|
|
|
|
where `ROUTE` and `VERB` are invented opcodes (lights to `LUM`, thermostat to
|
|
`THRM`, on to `IGNITE`, off to `DOUSE`, query to `SCRY`, set to `BIND`, cancel to
|
|
`BANISH`) and `ARG` is the request's target, extracted verbatim. Novelty is
|
|
guaranteed by construction: no pre-trained model has seen this frame or these
|
|
opcodes. Success is checked by a strict deterministic oracle that requires an
|
|
exact match on route, verb and arg, so there is no judge and no partial credit.
|
|
The prompt tells the model to emit a SIGIL directive but reveals none of the
|
|
mapping, so the base has no way to produce it. Observed base accuracy on the
|
|
held-out set: **0.000**. It emits plausible guesses like `SIGIL_DOOR_CHECK`,
|
|
never the protocol.
|
|
|
|
**3. Train a LoRA on C, base frozen.** A small synthetic dataset (a few hundred
|
|
request-to-directive pairs, `demo/dataset.py`) trains a PEFT LoRA adapter. Only
|
|
the adapter learns: every base parameter has `requires_grad=False`, and to prove
|
|
the base is untouched the script SHA-256 hashes the base weight shard before and
|
|
after training and shows the digests are byte-identical. The held-out eval set
|
|
uses locations and values that never appear in training, so a high score means
|
|
the protocol was learned and generalises, not that strings were memorised.
|
|
|
|
**4. Show base + LoRA does C.** The same held-out eval, now with the adapter
|
|
attached, scores **0.925**. This is the before-and-after that proves the adapter,
|
|
and only the adapter, carries the capability.
|
|
|
|
**5. Package the LoRA as an encrypted licensed unit.** The adapter 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 (root, organisational signer, leaf capability
|
|
certificate) and the unit is licensed under the leaf. The sealed unit is pure
|
|
ciphertext, safe to copy; an attacker's byte-for-byte copy is taken here for use
|
|
in step 7.
|
|
|
|
**6. Serve under a valid licence.** Presenting the leaf capability certificate,
|
|
the key authority verifies the chain, releases the wrapping key, and the adapter
|
|
is decrypted at load time, attached to the base, and runs C at **0.925**. The
|
|
same step shows the exfiltrated ciphertext, without the wrapping key, cannot be
|
|
decrypted at all: a guessed key fails AES-256-GCM authentication.
|
|
|
|
**7. Revoke, then crypto-erase.** Two rungs on the real sealed adapter.
|
|
Access-gated first: the authority withholds the wrapping key, the licensed load
|
|
is denied, and re-releasing restores it (this rung is reversible on purpose).
|
|
Then crypto-erasure: the authority destroys the wrapping key. The original sealed
|
|
unit and the exfiltrated copy are now both permanently undecryptable, the ledger
|
|
refuses to reinstate the licence, and the base alone scores **0.000** on C again.
|
|
The capability is gone and the escaped encrypted copy is inert forever.
|
|
|
|
---
|
|
|
|
## The mechanism layer: fast crypto unit tests
|
|
|
|
The cryptography that steps 5 to 7 use is also covered by a fast, GPU-free unit
|
|
suite that exercises each primitive on small stand-in blobs. This is the
|
|
mechanism layer: it runs in seconds on any machine with two pinned dependencies,
|
|
and it is where the crypto is asserted in isolation from the model. It is
|
|
secondary to the end-to-end demo above, not a substitute for it.
|
|
|
|
```sh
|
|
python3 -m venv .venv
|
|
. .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
./run_all.sh
|
|
```
|
|
|
|
`run_all.sh` runs four examples (each prints its evidence and `RESULT: PASS`)
|
|
and the pytest suite:
|
|
|
|
| Primitive | Example | Tests |
|
|
|-----------|---------|-------|
|
|
| Ed25519 three-tier certificate chain, offline verification, tamper/expiry/role checks | `examples/issue_certificate.py` | `tests/test_certificates.py` |
|
|
| Signed output-to-composition provenance, invalidated live by revocation or erasure | `examples/verify_output_provenance.py` | `tests/test_provenance.py` |
|
|
| Three-rung revocation ladder (soft, access-gated, crypto-erasure) with recovery paths | `examples/revoke_three_rungs.py` | `tests/test_revocation.py` |
|
|
| Crypto-erasure defeats an exfiltrated copy: both die after key destruction | `examples/crypto_erasure_undecryptable.py` | `tests/test_erasure.py` |
|
|
|
|
The crypto-erasure test is the strongest and cleanest because it rests on
|
|
standard cryptography, not on model behaviour: AES-256-GCM ciphertext without its
|
|
key is not degraded or obfuscated, it is gone.
|
|
|
|
---
|
|
|
|
## How the crypto works
|
|
|
|
Four small modules in `src/capability_licensing/`, each written from scratch on
|
|
the `cryptography` package.
|
|
|
|
- **`certificates.py`** applies the browser certificate-chain idea to model
|
|
capabilities: a self-signed root signs an organisational signer, which signs
|
|
per-capability leaves, all Ed25519 over a canonical encoding, verified offline
|
|
against a caller-held trust store. A leaf cannot issue certificates; the
|
|
authority does not retain leaf private keys.
|
|
- **`envelope.py`** is the AES-256-GCM envelope: a fresh data key per unit,
|
|
wrapped by a per-unit wrapping key. Both layers are authenticated, so a wrong
|
|
key fails closed rather than yielding garbage, and the unit identity is bound
|
|
in as associated data.
|
|
- **`revocation.py`** and **`keystore.py`** are the three-rung ladder. Soft
|
|
revocation is an advisory list entry (reversible). Access-gated withholds the
|
|
wrapping key at the authority (reversible). Crypto-erasure destroys the
|
|
wrapping key (irreversible by construction: the code refuses to re-release or
|
|
reinstate what no longer exists). The asymmetry is deliberate and stated:
|
|
the first two rungs depend on parties honouring the authority, only the third
|
|
is final at the level of the cryptography.
|
|
- **`provenance.py`** binds an output, by digest and Ed25519 signature, to the
|
|
certificates of its composition, and re-checks that binding against the live
|
|
authority state, so an output flips from valid to invalid the moment a
|
|
referenced certificate is revoked or erased.
|
|
|
|
For the walkthrough the certificate authority, revocation ledger and
|
|
wrapping-key custody are persisted to a gitignored `state/` directory between
|
|
commands, using each module's `export_state`/`restore` methods. Those files hold
|
|
key material in the clear: demo-grade custody, which is exactly why `state/` is
|
|
never committed.
|
|
|
|
---
|
|
|
|
## Honest scope and bounds
|
|
|
|
State the edges plainly. None of these undermine the demonstrated core; they
|
|
mark where it ends.
|
|
|
|
1. **The decrypted adapter is in memory while serving.** During a live licence,
|
|
the plaintext adapter exists in process memory and, on GPU, in VRAM. A
|
|
privileged host could read it out during that window. Sealing that window
|
|
needs a hardware secure enclave (a memory-encrypting CPU and a
|
|
confidential-computing GPU); that is the funded next step, not something this
|
|
artefact claims. What is demonstrated is different and still real: revocation
|
|
protects the unit at rest and every future load, and makes an escaped
|
|
*encrypted* copy permanently inert.
|
|
|
|
2. **Erasure removes the sealed unit, not "the capability from the base model".**
|
|
Crypto-erasure destroys a capability that was factored into a separate
|
|
encrypted unit at build time. Whether a capability can always be factored so
|
|
cleanly that no residual remains in the base is a separate research question.
|
|
The honest phrasing is that erasure removes the licensed unit with a
|
|
cryptographic guarantee, and here the capability lived entirely in the
|
|
removed adapter (the base scored zero before it and zero after it).
|
|
|
|
3. **The erasure guarantee is scoped to key destruction.** If the wrapping *key*,
|
|
rather than the ciphertext, had been copied out before erasure, destroying the
|
|
authority's copy would not help. Key custody (external key-stores, hardware
|
|
modules, backups that cannot resurrect a destroyed key) is named open work.
|
|
|
|
4. **Containment, not alignment.** This bounds what a system can do and caps the
|
|
blast radius of a revocation against an auditable, cooperating licensee. It
|
|
does not make a model safe, and it does not stop a determined owner of the
|
|
machine.
|
|
|
|
5. **Research artefact, not a production service.** The code favours readability
|
|
over hardening: keys live in process memory, there is no side-channel work and
|
|
no security audit. Its job is to make the mechanism checkable. Do not deploy
|
|
it.
|
|
|
|
---
|
|
|
|
## Claim provenance
|
|
|
|
Every substantive claim below, mapped to how you can check it. **Demonstrated
|
|
here** means you run it in this repository and read the observed number.
|
|
|
|
| # | Claim | Status | Evidence |
|
|
|---|-------|--------|----------|
|
|
| 1 | A 494M open base model scores 0.000 on the invented capability C. | Demonstrated here | `demo/02_eval_base.py`; observed 0/80 |
|
|
| 2 | A LoRA teaches C while the base weights stay byte-identical (SHA-256 verified). | Demonstrated here | `demo/03_train_lora.py`; base shard unchanged |
|
|
| 3 | Base + LoRA scores 0.925 on a held-out set with unseen arguments. | Demonstrated here | `demo/04_eval_lora.py`; observed 74/80 |
|
|
| 4 | The adapter, sealed as an AES-256-GCM unit under an Ed25519 leaf certificate, decrypts under a valid licence and runs C at 0.925. | Demonstrated here | `demo/05_package_unit.py`, `demo/06_serve_licensed.py` |
|
|
| 5 | Without the key, the exfiltrated ciphertext cannot be decrypted (fails AES-256-GCM authentication). | Demonstrated here | `demo/06_serve_licensed.py`, `demo/07_revoke_erase.py` |
|
|
| 6 | After the wrapping key is destroyed, the original and an exfiltrated copy are both permanently undecryptable, and the base scores 0.000 on C again. | Demonstrated here | `demo/07_revoke_erase.py`; observed 0/80 |
|
|
| 7 | The certificate chain verifies offline; tampered, expired and wrongly-issued certificates fail. | Demonstrated here | `examples/issue_certificate.py`, `tests/test_certificates.py` |
|
|
| 8 | Signed output provenance flips valid to invalid on revocation or erasure of a source. | Demonstrated here | `examples/verify_output_provenance.py`, `tests/test_provenance.py` |
|
|
| 9 | Memory-sealing / VRAM protection during a live licence. | Open (needs confidential-computing hardware) | future work, stated in the future tense |
|
|
|
|
The same mechanisms have been driven at larger scale, on an 8B open-weights base,
|
|
inside the research programme; this repository stands on its own reproducible
|
|
demonstration at small scale and does not lean on that run for its claims.
|
|
|
|
---
|
|
|
|
## Where the same shape goes next: a capability tree
|
|
|
|
The single-capability demo here is the N=1 case of something the certificate
|
|
chain already models. The natural scale-up keeps the *same small base model*
|
|
(the point is the number of capabilities, not the number of parameters) and
|
|
licenses a large number of distinct capabilities, say a hundred or more, each as
|
|
its own encrypted adapter under the certificate authority.
|
|
|
|
Arrange them as a **capability tree** rather than a flat list. The root authority
|
|
sits at the top; beneath it, organisational or parent-capability certificates
|
|
each own a branch (for example a "home-automation" parent over SIGIL-style
|
|
control leaves, a "financial-tooling" parent over its own leaves, and so on);
|
|
each leaf licenses one encrypted capability unit. This is exactly the shape the
|
|
three-tier chain in `certificates.py` already implements, extended in breadth and
|
|
one level in depth.
|
|
|
|
The removal story then follows the branches of the tree. Each parent capability
|
|
holds the key material that wraps the units beneath it, so revoking a parent
|
|
performs branch-wise crypto-erasure: destroy the parent's wrapping key and every
|
|
leaf unit under that branch becomes undecryptable at once, while sibling branches
|
|
keep working untouched. You would verify it the same way this repo verifies N=1,
|
|
one rung down the tree: after revoking a branch, every unit under it raises
|
|
`KeyDestroyedError` on decrypt and its capability eval drops to base level, while
|
|
a unit under a sibling branch still decrypts under its licence and still runs its
|
|
capability at full accuracy. That is the working single-capability demonstration,
|
|
grown into a governable tree of a hundred-plus licensed capabilities over one
|
|
model, with revocation that can take out a whole branch or a single leaf.
|
|
|
|
Also open, and named honestly: the memory-sealing work from the bounds section
|
|
above, harder capability pairs, per-span output attribution, and context-source
|
|
(as opposed to weight-source) provenance as a measured result.
|
|
|
|
---
|
|
|
|
## Repository layout
|
|
|
|
```
|
|
README.md this file
|
|
LICENSE MIT
|
|
requirements.txt crypto-only mechanism layer: cryptography, pytest
|
|
requirements-demo.txt full demo: torch, transformers, peft, ...
|
|
run_demo.sh the end-to-end demo, steps 1 to 7
|
|
run_all.sh the fast crypto examples, then the test suite
|
|
Makefile the crypto mechanism layer as make targets
|
|
src/capability_licensing/ clean-room crypto (from scratch on `cryptography`)
|
|
certificates.py Ed25519 three-tier authority + offline verification
|
|
envelope.py AES-256-GCM envelope encryption of capability units
|
|
revocation.py the revocation ledger (soft rung)
|
|
keystore.py wrapping-key custody (access-gated + erasure rungs)
|
|
provenance.py signed output-to-composition binding
|
|
demo/ the end-to-end demonstration
|
|
capability.py capability C: the invented SIGIL protocol + oracle
|
|
dataset.py the synthetic training and held-out eval data
|
|
common.py model loading, prompting, and evaluation
|
|
cryptostate.py persist the crypto authority across the demo steps
|
|
01_download_base.py ... 07_revoke_erase.py the seven walkthrough steps
|
|
examples/ one runnable script per crypto primitive
|
|
tests/ pytest suite for the crypto primitives
|
|
models/ outputs/ state/ gitignored; regenerated by run_demo.sh, never committed
|
|
```
|
|
|
|
## What is committed, and what is not
|
|
|
|
The repository ships **code only**. The base model, the Hugging Face cache, the
|
|
trained LoRA adapter, the wrapping keys, and every sealed (encrypted) capability
|
|
unit are all gitignored and never committed. A reviewer downloads the base and
|
|
trains the adapter themselves by running the walkthrough. See `.gitignore`.
|
|
|
|
## Requirements
|
|
|
|
- Mechanism layer: Python 3.10+, `cryptography` and `pytest` (pinned in
|
|
`requirements.txt`). No GPU, no model download.
|
|
- Full demo: the packages pinned in `requirements-demo.txt`
|
|
(`torch`, `transformers`, `peft`, and friends). A CUDA GPU is recommended; the
|
|
reference run used an RTX 4090.
|
|
|
|
## Licence
|
|
|
|
MIT. See `LICENSE`.
|