Two questions,
two signatures.
Are these bytes what the maker shipped? is authenticity. May this person run them, here, today? is entitlement. They get separate keys, separate signatures and separate failure modes.
A container is authentic forever, on a laptop that has never seen a network. An entitlement is per-person and revocable within days. One signature cannot do both jobs.
Who holds what
Five keys, each with one job. A player compiles in the service public key and a trust list of vendor public keys, and holds no private key of its own.
- Vendor key
- Held by the product’s author, offline. Signs the container digest list. Checked on every load, offline, forever.
- Service key
- Held by PRVNZ. Signs entitlements. Rotated. Checked on every load, from cache, while offline.
- Account key
- The holder’s MPC wallet. Signs asset transfers and settlement. Never on the load path.
- Device passkey
- One machine’s platform authenticator. Authorises entitlement requests from that machine, at issue and renewal.
- Device key
- Minted in the machine’s secure element and non-extractable. It is named by an entitlement rather than used to sign one.
Everything the load path does with these is verification, which is why none of it needs an RNG. A passkey proves who is asking when an entitlement is issued. It never signs a container and never proves entitlement on its own — a lost passkey is a re-enrolment, not a lost licence.
Container authenticity
A signed container carries its signature in its own entry, prvnz.sig — the only entry excluded from its own digest list, and the only one carrying no product content.
Every other entry is digested with SHA-256 and named in the signature. An entry present in the container but missing from the digest list is a refusal. Only containers can be signed: a loose document has no entries, so one claiming a signature is refused rather than half-checked.
{
"prvnz": 1,
"alg": "ed25519",
"key": "<base64, 32-byte vendor public key>",
"value": "<base64, 64-byte detached signature>",
"digests": {
"manifest.json": "<sha256, lowercase hex>",
"models/lead.nam": "<sha256, lowercase hex>"
},
"receipt": { "head": { … }, "inclusion": { … } }
}Signatures are computed over a byte string built by rule, never over the JSON. The product id and version sit inside the signed bytes, so a signature cannot be lifted from one product onto another or from an old version onto a new one. The algorithm is bound in alongside them.
prvnz-pack-v1\n <alg>\n <id>\n <version>\n then, per digest entry, sorted by path: <path>\x00<digest>\n
A verifier refuses on the first failure, in order: the file parses; the algorithm is ed25519; every entry is digested; every digest matches; the signature verifies; the transparency receipt checks out if present; and the key is trusted at the moment the receipt dates.
That last ordering is what makes a key compromise survivable. Revoking a vendor key sets its not_after in the trust list, and every container that key signed before that instant still opens. Zip metadata — timestamps, ordering, compression — is not covered, so a container rebuilt on a different day is still the same container.
Entitlement
An entitlement is a signed document cached on disk. It names an account, a machine, and the items it covers.
{
"prvnz": 1,
"alg": "ed25519",
"key": "<base64, 32-byte service public key>",
"value": "<base64, 64-byte detached signature>",
"claims": {
"account": "did:tenzro:human:…",
"device": "did:tenzro:machine:…",
"device_key": "<base64, 32 bytes>",
"items": [
{ "salt": "<base64, 16 bytes>", "digest": "<sha256(salt ‖ id), hex>" }
],
"issued_at": 1753600000000,
"expires_at": 1756192000000,
"not_after": 1785136000000,
"grace_ms": 172800000
}
}Every timestamp comes from the service. The optional fields — not_after, grace_ms and device_key — are signed present or absent, as an empty line rather than an omitted one. A term cannot become perpetual, a short grace cannot become a long one, and a hardware-bound entitlement cannot be relaxed into a soft one by deleting a field.
Item ids never appear in plain. Each carries a 16-byte salt minted fresh per item and per issuance, so the same product is a different digest for every holder and every renewal. Anything that reads the file learns nothing about the library in it.
The device allowance is not in the file. A number a client can read is a number a patched client can edit, so the count lives on the ledger and the file carries only one machine’s copy of the answer.
- account
- A TDIP DID and MPC wallet. Owns the entitlement, outlives every machine, and is what settlement keys against.
- device
- A delegated identity under the account. Copying the cache to another computer does not carry the licence with it.
- device_key
- A 32-byte identifier for a keypair inside the machine’s secure element. The private half cannot be exported, so it does not travel with the folder.
- expires_at
- A refresh deadline. Overrunning it is a connectivity problem, so the grace applies.
- not_after
- The end of the licence itself. No grace crosses it. Absent means perpetual.
Registering a machine
Registering provisions two levels of identity: the account, which owns everything, and one identity per machine authorised to act for it.
Enrolment happens in an application, not inside a plugin. On macOS, entitlements are a property of the process and come from the main executable’s code signature, so a bundle loaded afterwards contributes none of its own. The application owns the ceremony, the network, the account and writing the cache. The library in the plugin verifies a file that is already there.
A machine with a platform authenticator — Touch ID, Windows Hello, an Android biometric — runs the whole ceremony locally, and the holder sees one prompt named after the application they opened. A machine without one displays a FIDO hybrid QR. The phone and the computer complete a Bluetooth proximity exchange, so a screenshotted code sent to another country does nothing.
Splitting account from machine is what gives revocation its granularity. A stolen laptop is one revocation against that device; the account, the wallet and every other machine are untouched. Entitlements are checked against the account, so buying on the desktop means it is present on the laptop with no transfer step.
Releasing a slot never requires the machine holding it. The stolen laptop, the dead drive and the sold desktop are the ordinary cases. Release is authorised by the account’s passkey from any enrolled device, and the released machine finds out at its next refresh.
No dongle, and no network
Opening a plugin does not ask anything for permission. The check is a local signature verification against a compiled-in public key, on a file already on disk.
There is nothing to plug in. A dongle is a bearer token, so it has to be present every session — a permanent tax, and one with no recovery path when it is lost. A passkey is needed once per machine, at enrolment. After that the machine refreshes against its own device key and the phone is uninvolved.
The grace period is two days past a server-issued expires_at, and it never starts from “no entitlement found” — nothing cached is a refusal. Long enough to cover a basement studio, a flight, or a hotel with bad wifi over a weekend.
A machine with no network at all is a delivery problem rather than an authentication one, because only the service key mints an entitlement. So the machine displays its device DID, its device key’s public half and a nonce as a QR; a phone carries it to the service and renders the issued entitlement back as a single static frame a laptop webcam reads. Where there is no camera, the same two files travel on any USB stick — once per machine, not every session.
Portability across machines runs on commodity hardware. The WebAuthn prf extension has a FIDO2 security key derive a 32-byte secret that cannot be obtained without the key physically present; the entitlement is encrypted under it and stored on any disk, including a USB stick. The file alone is inert. Any FIDO2 key works, and losing it costs portability rather than access.
Canton, off the load path
Canton is the ledger of record. Nothing on the load path waits on it. Settlement is asynchronous, and its failure mode is a delayed ledger entry rather than an amp that stopped working mid-set.
- The ledger holds
- The licence — its term, its device allowance, which machines hold slots, and who owns it.
- The file holds
- One machine’s copy of the answer, signed, valid until it expires.
- The ledger changes
- On purchase, renewal, transfer, or a device added or dropped.
- The file changes
- At refresh, when the service re-reads the ledger and re-signs.
- If Canton is unreachable
- Issuance and transfers wait. Nothing on the machine waits.
Slots are held against the account rather than the product, so a machine allowance is spent across a whole library instead of per plugin. A licence with no stated allowance is every machine the holder owns; each still enrols and still carries its own device key.
Transfer policy is a property of the licence template, set per product by its maker: whether resale is allowed, whether it needs approval each time, what commission is taken and how it splits, and what happens to the term when the licence changes hands. Written in Daml, a transfer is a choice on the licence contract. It carries the authority that contract requires — the holder’s, the maker’s, or both — and the whole transaction commits or none of it does. The licence moves and the maker is paid in the same transaction, so there is no window in which the buyer holds the licence and the maker holds an invoice.
Two properties come from the ledger rather than from us. Authority — a licence changes hands only with the authorisation its own contract demands, checked when the transaction is validated rather than requested by a policy document. Every party to a transfer signs for the part they are a party to, and a transaction without the authority it needs does not commit. Privacy — a transaction is shown to its parties and to nobody else. A holder’s library is not a public record, and a catalogue cannot be reassembled by an observer who was never party to it.
What the ledger contributes beyond storage is that a licence’s history is verifiable by someone who is not the vendor. That matters least for whether a plugin opens and most for everything downstream — a resale needing proof the seller held what they sold, a payout needing proof of what was licensed when, a dispute where the vendor is an interested party.
There is one ledger and no second chain. There is no PRVNZ coin and nothing to trade. Reach across other networks comes through Tenzro, which supplies the network and the settlement client; PRVNZ owns the security and licensing side.
How it goes into a product
Two files and one call. A signed container ships with the product; an entitlement is written by the enrolment application; the library verifies both where the work opens.
build → sign the container with the vendor key
→ log the signature, attach the receipt, publish
enrol → the application runs the ceremony, mints the device key,
requests the entitlement, writes the cache
load → verify the container against the trust list
→ verify the entitlement against the service key
→ check the item digest, the device, the deadlineIn-process formats — VST3, CLAP, Audio Unit — read the cache at a path the integrator supplies. Build that path from the account database rather than from the environment: a sandboxed host has HOME rewritten to its container, so the usual application-data helper lands somewhere the enrolment application never wrote. Resolve the real home instead.
AUv3 runs in its own process with its own container, and an App Group is bound to a Team ID. A vendor shipping AUv3 ships their own enrolment application under their own Team ID, sharing a group with their own extension. Vendors shipping only in-process formats need none of that.
Nothing about this is audio-specific. The container format takes any payload, the item ids are in whatever namespace the integrator already uses, and the entitlement treats them as opaque. The same two layers cover a plugin, a font, a 3D asset, a film cut, a photograph, a manuscript or a training set.
How you reach it
Standalone app
Enrols a machine, holds the account, and lists everything you own. You open it and quit it. No daemon, no driver, no kernel extension, and no process left running once it closes.
SDK
A native library you compile into your own software. It verifies a container and an entitlement from files already on disk — no network, no window, no prompt. Verify-only, so it holds no private key and builds for wasm32 alongside desktop targets.
API
The issuing side. Issue and revoke entitlements, mint the passports they are checked against, read the ledger state behind a licence, and drive transfers and splits. This is what a label, a store or a manufacturer runs.
CLI
Signing and packing at build time. Generate a vendor key, sign a container, submit the signature to a transparency log and attach the receipt — the steps that belong in a release pipeline rather than in an application.
MCP server
The same issuing surface exposed to agents. Query a catalogue, check what a holder is entitled to, and issue against a template, under the same authorisation the API uses.
What a refusal means
Every refusal names the party responsible for it, because sending someone to the wrong place is worse than saying nothing.
- UnknownVendor
- The container is internally consistent and signed by a key this player does not trust. A trust decision, not corruption.
- DigestMismatch
- An entry does not match what was signed, or an undigested entry is present. The container was altered after signing.
- UnknownLog
- A transparency receipt is present and does not check out against a log this verifier accepts.
- NoEntitlement
- No cached entitlement for this account. A refusal, never a grace — the clock does not start from nothing.
- WrongHardware
- The entitlement names a device key this machine cannot produce. The cache was copied from another computer.
- Expired
- Past expires_at and past the grace, or past not_after, which no grace crosses.