1. Introduction
Note: This chapter is non-normative.
1.1. Motivation
At the time of writing, digital marketplaces are rife with policies and software that are hostile to users, both content creators and consumers. As an internet user you have likely encountered:
Digital Rights Management (DRM). To combat piracy, many digital stores will apply DRM which limits how you can use some content. This usually implies that you can only access the content from a single service, and you can never own the content in perpetuity. If the service dies, you lose access to what you paid for. We have long known that DRM is only a speed bump for pirates, while paying customers are the only ones inconvenienced by DRM. People who pay most often want to support the creator, and they deserve the best experience, which means letting them download content free of restrictions.
Products rescinded after purchase. Some stores are so anti-consumer that they will rescind access to purchased content simply because the store’s re-licensing rights have expired.
Platform lock-in. To access the content you want, you likely need many accounts on various different services, each with its own unique limitations. Even when the content is DRM free, you still need to interact with multiple services to download your entire library. This can also pose a problem for sellers, because they are at the mercy of a centralized algorithm that determines how consumers discover content.
Compromised creator/artist identity. Particularly on music streaming services, artist pages are routinely under attack by scammers; they upload fake content under an alias and then it shows up on a real artist’s page, which is frustrating for both fans and artists.
Large-scale data leaks. In the case of streaming services, even with DRM, we have witnessed how easy it is to bypass restrictions and rip entire catalogs of content. See the great Spotify scrape of 2025. This occurred because of the music streaming subscription model, which made it easy for paying users to cheaply scrape far more value than they paid for. They didn’t even need help from a Spotify employee!
The Fondachi protocol solves all of these problems, and because it’s an open standard, existing digital stores can even adopt it and join the federated marketplace. The goal is to empower creators and consumers by building a decentralized network that frees them from lock-in while still protecting the integrity of content and ownership.
1.2. Design Goals and Non-Goals
1.2.1. Goals
-
Freedom from DRM. Purchased content is licensed for perpetual ownership.
-
Decentralized/federated marketplace
-
End-to-end encryption of content such that it is only accessible by those with proof of ownership, making large-scale scraping financially infeasible. This includes service operator blindness.
-
Strong integrity of creator identity. Creators should have full control over their market presence and payment options.
-
Limit downloads so account sharing does not turn a Fondachi store into cheap infrastructure for pirates
-
Portable identity. Users should hold custody of their identity so they can migrate their catalog or library to a new instance if they desire.
-
Verifiable authenticity of code that manages a user’s keys, by using WEBCAT or WAICT. This is a requirement to enforce operator blindness.
1.2.2. Non-Goals
-
Prevent piracy. This is fundamentally incompatible with freedom from DRM. The best we can do is avoid the service itself being exploited.
-
Publicly distributed storage infrastructure. While this works well for Bittorrent, it would make it impossible to limit downloads.
-
Distributed consensus. There is no reason to complicate this with blockchains.
1.3. Scope of This Document
The scope of this document is to define the protocol unambiguously so it can be implemented by anyone who wants to operate a peer in the Fondachi network: the identity root and its rotation (§ 4 Identity and Keys), the canonical encoding and every signed object (§ 5 Canonical Encoding and Object Model), content encryption (§ 6 Content Encryption), the protocol flows (§ 7 Protocol Flows), the HTTP API (§ 8 HTTP API), federation (§ 9 Federation), the security considerations (§ 10 Security Considerations), the registered identifiers (§ 11 Registrations and Well-Known Resources), and the byte-exact test vectors that are the conformance surface of each draft revision (Appendix A. Test Vectors).
The following are deliberately left out of scope:
-
Cross-host settlement. A peer minting grants under another creator’s authority, and money moving between hosts, are not defined. The sale always executes on the origin host, which collects payment; a buyer’s home host brokers the purchase and files the resulting portable grant, so only the grant crosses the boundary (§ 7.3 Cross-Host (Brokered) Purchase, § 9 Federation).
-
Payment processing. Rail adapters are host-internal. The protocol defines only the public webhook surface and the settlement and reversal events that feed the license engine (§ 8.6 Rails).
-
Storefront presentation and discovery ranking. How a work is displayed, how a host surfaces or ranks it, and the preview material shown before purchase are host concerns, not signed catalog data (§ 5.3.3 Release (fondachi/release)).
-
The text of the Standard License Terms. Grants bind to a versioned terms document by identifier; the terms themselves are published separately (§ 5.3.7 LicenseGrant (fondachi/license-grant), § 11.3 Names and Prefixes).
-
Moderation policy. The protocol defines takedown and delist mechanics and how they propagate, never what ought to be taken down (§ 7.10 Takedown Propagation).
-
Key storage products and custody user experience. The protocol sees only DIDs and signatures, and requires that private keys never leave the client side (§ 4.3 Custody). Which keystore, sync arrangement, or hardware a client uses to hold them is its own affair.
-
Concurrent multi-writer catalogs. Replicating a catalog between peers is in scope; writing to one from more than one place is not. Every catalog has a single authoritative writer, and the copies peers hold by gossip are non-authoritative caches of it. Letting several peers publish into the same catalog independently, which would require union-merge and fork detection to reconcile, is a compatible future extension the object model already admits, but it is not specified here (§ 9 Federation).
2. Protocol Overview
Note: This chapter is non-normative.
This chapter introduces the protocol at a high level before the normative chapters define it in detail.
2.1. Setting the scene
In our cast, we have:
-
Carla, a musical artist who wants to sell her latest album on the Fondachi network
-
Bob, a fan of Carla’s music who wants to purchase the album
-
Two web servers, B and C, that are peers on the Fondachi network. We refer to them as hosts. They are each responsible for serving their own storefront app. They do not necessarily run the same software or have the same user base, but they are both compliant with the Fondachi protocol. Because they are peers and know how to cooperate via the protocol, we say that they are federated.
2.2. Carla onboards as a creator on host C
Carla decides that she trusts host C to host and sell her album. Since it’s
her first time using the Fondachi network, she uses host C’s web app to
create her identity. The app makes it clear that she is identified by a unique
decentralized ID called a did:fnd. She is also assigned a cryptographic key
pair for signing digital documents, but the private key is only visible to her
browser, not host C. She is also prompted to download a recovery phrase and keep
it somewhere safe, like a password manager.
The app reassures Carla that her private key is never visible to the host server. Because Carla is careful about her online security, she wants to verify this claim. The app has a link to its own open source project page, and because the host implements WEBCAT/WAICT, Carla’s browser indicates that the host is indeed serving her the very same source code revision from the project’s latest release. Because of the app’s popularity and open source stance, she feels that if they were exfiltrating keys, someone would have noticed.
The app also guides Carla through becoming a creator and making host C her home host, meaning it will store Carla’s content catalog and sell from it on her behalf. The app explicitly asks for Carla’s permission to do this, and as a result, the app uses her private key to digitally sign a delegation: a narrow, revocable permission to publish and sell on her behalf.
On the backend, host C provisions a new sealing key for Carla via a key management service (KMS). This key will be used later for releasing content decryption keys to buyers of her music. Because the private half of the sealing key has the power to unseal content keys, host C cannot access it. This is guaranteed by the KMS, and host C allows Carla to inspect the KMS policy to verify this. The app also asks Carla to approve the use of this sealing key, after which the app uses her signing key to sign another authorization document called the SealingKeyDeclaration.
Behind the scenes, the app derives one more secret from Carla’s recovery phrase: her content master secret, from which the encryption key for each of her works will later be derived. Like her private key, it never leaves her device; not even host C will ever be able to read the content it stores for her.
Finally, because host C will be collecting payments on her behalf, Carla connects a payout account through whatever payment rails host C supports. The particulars are between her and host C; the protocol only cares that a sale settles, and that the settlement can be checked later.
Now Carla is ready to start using the app to publish her music.
2.3. Bob onboards as a buyer on host B
Bob goes through a similar process on host B, but he is not trying to sell anything, so he only needs to generate his identity keys and back up his recovery phrase.
The app explains that host B will be Bob’s home host, meaning it will broker purchases with other hosts on the network and keep track of what Bob owns in his content library. This arrangement is only for convenience; Bob will still have the ability to download all of his content DRM-free and maintain his own identity and ownership records, which are portable to any host, or to no host at all.
Now Bob is ready to browse the marketplace and purchase his favorite artist’s content.
2.4. Carla publishes a music album on host C
Carla submits her album files for upload via the app. While the app processes her files, she fills out a form to describe any necessary metadata and choose an offer price. In the background, the app encodes her tracks into the formats she wants to offer (lossless and MP3, say), splits every file into chunks, and encrypts each chunk before uploading it. The unencrypted files never leave Carla’s device.
The app uses her signing key to sign a few documents required for publishing the album:
-
Work. A stable identity for the album itself.
-
ContentManifest. Describes the content files included in the work, as encrypted chunks.
-
Release. Describes how the album will be presented in the store.
-
Offer. Sets the price of the release and time span it will be available.
Host C receives only ciphertext and signatures. It can still check that the upload is complete and intact, because every encrypted chunk must hash to an address listed in Carla’s signed manifest. The album’s decryption key travels inside the manifest too, wrapped to the sealing key from Carla’s onboarding, so the only thing that can ever release it to a buyer is the sealer service she approved.
With the ciphertext stored, host C appends the signed documents to Carla’s identity log: an append-only, signed record of everything she publishes, which any peer can replicate and verify. From this moment the release is public. Other hosts on the network, host B included, can see that Carla’s album exists and is for sale.
2.5. Bob purchases and downloads the album
Bob uses the app to browse the catalog of all content from hosts federated with host B. He finds Carla’s album, because host B regularly crawls the catalogs of its federated peers.
When Bob tries to buy the album, host B brokers a guest checkout at host C, presenting Bob’s DID as the grantee. Payment settles at C. Then host C invokes the sealer to collect the album’s content key, sealed with Bob’s public key so that only Bob can unseal it with his private key. The sealer independently verifies that the payment settled and matches Carla’s signed offer price, so host C cannot mint content keys for sales that never happened. Once host C has the sealed content key, it bundles all of the data Bob needs into a LicenseGrant document:
-
Bob’s DID
-
Carla’s signed release document
-
a reference to the settled payment
-
the sealed content key
Host C signs the grant with its sales key, under the delegation Carla signed at onboarding, and sends it back to host B. Before filing it into Bob’s library, host B verifies the grant’s chain of signatures back to Carla’s own key, using only the signed documents themselves. Bob sees this flow complete successfully and the app offers him a download. When he starts it, the app uses the grant and Bob’s signing key to produce a short-lived RetrievalProof, which it presents to host C to authorize the download of Carla’s album.
The download process occurs via the app because only ciphertext can be fetched from host C. As the app receives each ciphertext chunk, it decrypts it using the album’s content key. In order to get the content key, the app needed to fetch Bob’s grant for the album, take out the sealed content key, and use Bob’s private signing key to unseal it.
Not only is the download completely DRM-free, but Bob can also export his library from the app and receive the grant for every work he owns. Each grant independently conveys his perpetual ownership of a work, and he can use them with another host or mirror to download his content again if needed.
2.6. Carla delists the album but Bob maintains ownership
After a good long run, Carla decides to take the album off the market for a while. In the app, she withdraws it from sale, and host C signs and registers a small document called a delist in her identity log. As the delist spreads through ordinary catalog sync, every host on the network stops selling the album; host B’s storefront now shows it as unavailable.
For Bob, nothing changes at all. A delist stops sales and nothing else: his grant is untouched, his files are still on his disk, and if he ever loses them he can still prove ownership and download the album again. He probably never notices that the album left the store. If Carla changes her mind next year, she can relist it just as easily.
2.7. Host B goes out of service and Bob migrates
A few years later, host B announces that it is shutting down. Being a well-run service, it gives everyone a couple of months of notice, so Bob’s app tells him well ahead of time and suggests choosing a new home host.
The migration turns out to be pleasantly boring. Bob’s identity is his key pair, which never lived on host B in the first place. His grants are signed documents his app already holds and can re-download from host B while it still answers. His music is on his own disk. So migrating amounts to importing his identity and library into the web app of a new host B'. Host B' verifies each grant chain for itself and takes over brokering purchases and tracking his library from there.
2.8. Host C goes out of service and Carla migrates
Eventually host C announces its own shutdown, and Carla has more at stake: host C sells her album, stores her encrypted content, serves her catalog, and operates the sealer she approved when she onboarded. Still, none of those jobs ever required custody of her keys, so all of them can be handed to a new host with signatures and copying.
Carla picks host C′ and the app walks her through the move. First she signs a MirrorAuthorization, a document that permits C′ to store and serve her catalog and encrypted content without granting it any authority to act for her, and C′ syncs everything across while C is still up. Next she signs a new delegation for C′ and revokes host C’s, with a grace window so sales can continue during the transition. Her identity log moves whole: C′ imports every entry, and Carla signs a small document called a LogContinuation naming the last entry of her log at host C. Her log was never only at host C: every peer that crawls her catalog holds a copy and knows where it ended. The continuation is Carla’s own signed statement of that endpoint, and her authorization for C′ to carry the log forward, so anyone can check that C′ picked up her whole history exactly as it stood rather than a truncated or substituted one. Finally, the app sets up a fresh sealing key with C′, approves it with a new SealingKeyDeclaration, and re-wraps the album’s content key to it; the encrypted files themselves are unchanged.
From Bob’s side, none of this is visible. His grant chains back to Carla’s key through the delegation that was live when he bought the album, and that chain stays valid forever; changing hosts does not rewrite history. The next time he downloads the album, his app simply finds it served from host C′.
3. Conventions
The requirements language of this document is the BCP 14 boilerplate of the Conformance section. All text is normative unless marked otherwise; where non-normative text appears to conflict with a normative requirement, the normative requirement governs.
3.1. Notation
Protocol object types are written as bare PascalCase names (ContentManifest,
KeyEvent) when the object itself is meant, alongside the plain English
noun (manifest, key event) where the narrative reads better; the two
are interchangeable. Code font is reserved for what appears literally on the
wire: type tags (fondachi/manifest), field names, and values.
Object schemas are written in CDDL, the Concise Data Definition Language
[RFC8610]: a small notation for describing CBOR data structures. The
primitive types are CDDL’s own: tstr is a text string, bstr a byte
string, and uint an unsigned integer. A leading ? marks an optional
member, [+ x] is an array of one or more x and [* x] of zero or more,
and / separates alternatives. Comments run from ; to the end of the
line. These shared rules are assumed by every schema in this document:
cid = #6.42 ( bstr) ; a CIDv1 link, as DAG-CBOR encodes links did = tstr; a DID: did:fnd for identities, did:key for keys multikey = tstr; a Multikey verification-key value timestamp = uint; integer UNIX seconds, UTC unknown-fields = ( * tstr=> any) ; every body's extension point
The cid, did, and multikey value spaces are specified where the
identifiers are (§ 5.1 Canonical Encoding, § 4.1 Identifiers,
§ 4.2 Keys and Signatures); unknown-fields marks the extension point whose rule
is given with the signature envelope (§ 5.2 Signature Envelope).
The terms this document uses are collected in Appendix B. Glossary.
4. Identity and Keys
This section defines how identities are named, how they sign, and how they survive the loss or compromise of a key. All of it is core except the external identity bindings (§ 4.5 External Identity Bindings) and the profile document (§ 4.6 Profile Document), which are optional extensions; the license model runs entirely on the DID.
4.1. Identifiers
Every identity in this protocol is a decentralized identifier (DID) [DID-CORE].
Every license-holding identity (every holder and every creator) MUST be a
did:fnd: the CID (§ 5.1 Canonical Encoding) of the identity’s genesis event, the
signed record that opens its identity log and names its first keys
(§ 4.4 Key Events (Rotation and Recovery)). Verifiers MUST reject any other DID method for a
license-holding identity.
did:fnd:<CID of the genesis key event, base32 CIDv1>
A did:fnd is self-certifying: the identifier is a hash of the record
that declares the identity’s keys, so possession of the DID and the
identity’s key events is possession of everything needed to verify its
signatures.
There is nothing to resolve and no directory to consult: the events travel
with the grant chain, so a chain rooted in a did:fnd verifies fully
offline and stays verifiable forever. The identifier never changes: keys
rotate under it (§ 4.4 Key Events (Rotation and Recovery)), and the DID a grant names is the DID its
holder keeps for life.
Keys themselves are written as did:key [DID-KEY] values encoding
Ed25519 public keys (§ 4.2 Keys and Signatures): the signing key and rotation
keys inside a key event, and the service keys of infrastructure.
Every other identifier in the protocol attaches to a did:fnd root;
none ever replaces one:
-
A host or mirror is identified operationally by its HTTPS origin, and advertises a service key, itself a
did:key, in its discovery document (§ 8.1 Discovery). A service key signs on behalf of infrastructure, and never holds or roots a license. -
A domain gives a creator a recognizable public identity ("the artist at example.band") through a verified domain anchor on their DID (§ 4.5.3 Domain Anchor), not by being the root itself.
-
atproto and ActivityPub identities attach as external identity bindings (§ 4.5 External Identity Bindings) for login, social reach, and verified badges.
Keeping the root self-certifying is what makes the ownership promise of this protocol unconditional: a license verifies with the bytes the holder already has, and no domain, directory, or server can lapse out from under it.
4.2. Keys and Signatures
The signing algorithm is Ed25519 [RFC8032]. It is REQUIRED: it is the
algorithm of every license-holding root, of every key in a grant chain, and
the only registered alg value in the signature envelope
(§ 5.2 Signature Envelope, § 11.6 Algorithm Identifiers and Parameters).
Verification keys (the public keys a verifier checks signatures against) are
expressed as Multikey values [MULTIKEY]: the key bytes, prefixed with a
multicodec tag identifying the algorithm, encoded as a self-describing
multibase string. This is the publicKeyMultibase form used in DID documents,
and it is also exactly how a did:key encodes its key.
did:key appears as a signing key in the key-event test
vectors of Appendix A. Test Vectors:
did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX
Taking the string apart, did:key: names the DID method; the leading z is the multibase
prefix for base58btc; and the string it introduces decodes to the multicodec
tag 0xed01 (Ed25519 public key) followed by the 32 key bytes. The tag is why
every Ed25519 did:key begins with z6Mk.
Two DID methods divide the work. A did:fnd names an identity and
never encodes a key; the keys that sign for it are declared by its key
events (§ 4.4 Key Events (Rotation and Recovery)). A did:key is a key: every signing key,
rotation key, and service key is one, so a verifier always has key bytes in
hand and no signature verification ever resolves a DID document. A service
key is bound to the endpoint it signs for by the endpoint’s discovery
document (§ 8.1 Discovery), not by a DID method.
The signing key is also the key that content keys are sealed to. A content key is the symmetric key a work’s bytes are encrypted under (§ 6.4 Content Keys); at purchase, the sealer encrypts it to the grantee’s signing key current at that moment, converted to its X25519 form, and the sealed key is carried inside the grant. A holder therefore needs exactly one live keypair, which both proves what they own and receives the keys to read it; after the signing key rotates, keys sealed to its predecessor are reobtained by redemption (§ 6.6 Key Redemption).
Signature validity is evaluated at issuance time: an object signed while its key was its identity’s signing key (§ 4.4.4 Verifying a Chain (Offline)) remains valid after that key is later rotated away. Grants do not die when keys rotate. The same rule governs objects signed under a delegation (§ 5.3.6 Delegation (fondachi/delegation) and Revocation): validity is judged against the delegation’s state when the signature was made, never retroactively.
4.3. Custody
The protocol only ever sees DIDs and signatures, but where private keys may live is not left open. Content encryption is end to end, and the operator-blindness guarantee (§ 6.3 The Blind Host Model) fails the moment an operator can reach a key that unseals content.
A host, mirror, or any other service operator MUST NOT generate, hold, or otherwise have access to an identity’s private keys. This applies to the signing key and to rotation keys (§ 4.4.2 Rotation Keys and Pre-Rotation) alike: a rotation key authorizes key events, and a key event hands its signer the identity, meaning signing rights and, by redemption, the content keys (§ 6.6 Key Redemption). Custody of a rotation key is therefore custody of the identity on a delay. Private keys are generated and held on the client side, and only signatures ever leave it.
A ciphertext of a private key is not custody of it, provided the operator has no way to the decryption key. An operator MAY store, on a holder’s behalf, a copy of the identity’s private key encrypted under a secret that exists only on the client, for example the key wrapped under the output of a passkey’s PRF extension [WEBAUTHN-PRF], which the authenticator evaluates on the client and never discloses to the relying party. This is how a holder reaches one identity from several devices without the operator ever being able to sign: every device that can use the passkey can unwrap the key, and the operator, holding only the ciphertext, cannot. The same arrangement MAY carry a rotation key, wrapped under a client-only secret such as a recovery phrase, and SHOULD be preferred over deriving the rotation key from that secret: a wrapping can be replaced when the secret is, where a derived key is the secret forever (§ 4.4.2 Rotation Keys and Pre-Rotation). A client-only secret that unwraps a rotation key SHOULD unwrap nothing else: rotation authority is exercised only through a registered key event, where a secret that also reaches the signing key can act silently and indefinitely. The operator MUST release such a ciphertext only to a client that has authenticated as its holder, so that the ciphertext is never an enumerable public object. The client MUST derive the wrapping key from the client-only secret with a KDF that is domain separated, for example HKDF with an application label fixed for this use. Domain separation gives each use of the secret a distinct key, so that a key derived for one use cannot open what was wrapped for another.
Note: The ciphertext is nonetheless an exfiltration target: the code that unwraps it runs in the same client that a served web application controls, which is exactly the case the code-integrity requirement below exists for. Nothing about storing the ciphertext with the operator changes that requirement or adds to it.
A web client that handles private keys MUST be covered by code-integrity verification, using WEBCAT [WEBCAT] or WAICT [WAICT] (Goal 7 in § 1.2.1 Goals, § 10.7 The Active-Custodian Problem (Operator Forgery)). The reason is that a key in browser storage is operated on by code the operator serves on every visit, so where the key sits does not settle who can use it. Without an attestation that the served code matches published code, the operator can alter the client and exfiltrate the key at will, and client-side custody is client-side in name only. A native client holding a key on disk does not have this dependency. Note also that the operator serving client code need not be the holder’s home host, and being a home host earns an operator no trust to serve key-handling code; § 8 HTTP API keeps these relationships distinct.
Note: Within the client side, arrangements vary, and the protocol is indifferent among them: a key file on disk, a platform keystore, a key synced between the user’s own devices in the manner of passkeys, or a hardware-backed key that signs without ever being readable. Whether a platform vendor’s synced keystore deserves the user’s trust is a question about their platform, outside this document’s scope. The custody rule is about the operators of Fondachi services, and it has no such latitude: they are never among the parties with access.
4.4. Key Events (Rotation and Recovery)
An identity’s keys change by key events (fondachi/key-event): signed
records, chained by CID, registered in the identity’s log
(§ 5.3.11 Identity Log). The first is the genesis event, whose CID is the
identity’s name (§ 4.1 Identifiers); each later one replaces the signing
key, the rotation keys, or the home host, under the DID that never changes.
A verifier walks the chain entirely offline: every key needed to check an
event is carried in the events themselves.
key-event-body = { ? identity : did , ; the did:fnd this event extends; absent in genesis ? prev : cid , ; the preceding key event; absent in genesis signing_key : did , ; the did:key that signs the identity's objects rotation_keys : [ * did ], ; cold did:keys that may authorize later events, strongest first next_rotation : [ * bstr.size 32 ], ; SHA-256 of each next rotation key's Multikey bytes, strongest first ? home_host : did , ; service key of the home host the identity commits to; absent means none ([[#registrar]]) issued_at : timestamp , unknown-fields , }
4.4.1. The Genesis Event
The genesis event carries no identity and no prev: it cannot name a DID
that is its own hash. It is signed by one of its listed rotation keys, or by its signing_key
if the list is empty, and the identity exists
the moment it is encoded: the DID is computable on the client that drew
the keys, before any service has been consulted.
A genesis event is registered at position 1 of the identity’s log, and only there. A rival log cannot open with a different key set for the same identity, because its first entry would not hash to the DID.
An identity that lists no rotation keys and no next_rotation commitments
is frozen at its genesis keys: nothing can ever be rotated or recovered,
and by the same token nothing can be seized through a key event. That is a
legitimate choice for a throwaway identity and a poor one for an identity
that roots a catalog, and since next_rotation can only be consumed, never
retrofitted, the choice is effectively made at genesis.
4.4.2. Rotation Keys and Pre-Rotation
Rotation keys are held cold, separate from the day-to-day signing key. What protects the identity is that separation, not the keys' existence: a rotation key stored beside the signing key falls with it. The custody rule of § 4.3 Custody applies to rotation keys in full, and for the reason given there: a rotation key is the identity on a delay, so no service operator may hold one.
Nothing requires the listed keys to be the identity’s own. rotation_keys
lists DIDs, and an identity MAY name keys held by parties it trusts: a
family member, or a second device kept somewhere else. A key given to an
heir is a will: the heir rotates the identity’s keys to their own and
holds everything it holds (§ 7.6 Inheritance (Takeover)). Custody may be
indirect: a rotation key escrowed under a client-only secret
(§ 4.3 Custody) is given by giving the secret, so a recovery phrase
written into a will is a rotation key given to an heir. This matters because the
two emergencies rotation keys serve are not equally served by a cold key in
a drawer. Rotation after a compromise works well; the user still holds
everything, and the cold key is what lets the root survive the breach
(§ 4.4.7 What This Buys). Recovery after total loss works only as well as the
separation the user actually maintained.
next_rotation is pre-rotation: each event commits, by digest, to the
rotation keys that come after these, strongest first. An event that changes
rotation_keys MUST reveal keys matching its predecessor’s commitments and
MUST be signed by one of the revealed keys (§ 4.4.3 Later Events). What
this buys is a bound on rotation-key theft: a thief holding every current
rotation key still cannot rotate the set, because the next keys were
chosen, and their digests fixed, before the theft. It buys that only as far as
the separation is real: next keys stored beside current keys fall with
them, so an identity SHOULD keep its pre-committed keys at least as cold as
its current ones.
4.4.3. Later Events
Every event after the genesis names its identity and its prev, and is
one of two kinds, distinguished by what it does to rotation_keys:
-
A maintenance event carries its predecessor’s
rotation_keysandnext_rotationunchanged, and replaces thesigning_key, thehome_host, or both. It MUST be signed by a key listed in the predecessor’srotation_keys. -
A rotation event replaces
rotation_keys. The new list MUST be exactly the preimages of the predecessor’snext_rotation, in order, and the event MUST be signed by one of those revealed keys. It carries a freshnext_rotation(possibly empty, which freezes the set), and MAY also replace thesigning_keyandhome_host.
A verifier MUST reject an event that is neither: a rotation that reveals keys the predecessor did not commit to is the seizure the commitments exist to prevent, whoever signed it.
Every event is a snapshot of the identity’s whole state, never a delta:
unchanged values are carried forward explicitly, one state has one
encoding, and an event that omits home_host commits to no home host,
whatever its predecessor named (§ 4.4.6 The Home Host as Registrar).
4.4.4. Verifying a Chain (Offline)
To establish the keys of did:fnd:X at any moment, a verifier
checks, using only objects presented alongside the grant chain:
-
the genesis event’s CID is
X; -
each later event names
Xasidentity, names its predecessor byprev, and satisfies the signing rules of § 4.4.3 Later Events against that predecessor; -
the chain contains no cycles and does not exceed 256 events.
The identity’s current signing key is the signing_key of the last
event in the accepted chain. Its signing key at a past moment, which is
what the at-issuance rule of § 4.2 Keys and Signatures verifies against, is the
signing_key of the last event issued at or before that moment. The
identity’s current home host is likewise the home_host of the last event
in the accepted chain, absent meaning none. Nothing is
fetched and no directory is consulted.
A verifier MUST reject a walk past 256 events rather than truncate it. The bound is a sanity limit on presented bundles: every event permanently lengthens the walk every later verification of the identity performs, and nothing ever shortens it. Key events answer compromise, loss, succession, and migration; an identity MUST NOT rotate on a schedule. Scheduled rotation spends what is never recovered and buys nothing under the at-issuance rule, since key age never invalidates a signature and a compromise is answered by rotating when it is found.
4.4.5. Registration, Conflicts, and the Recovery Window
Key events are registered in the identity’s log, and their log
positions are their registration order. Among validly signed events naming
one prev, the one registered earliest wins, and a registered event beats
an unregistered one. The one exception is the recovery window:
For 72 hours after an event’s registered_at, a conflicting event naming
the same prev and signed by a strictly stronger key of that
predecessor displaces it, and with it every event built on it. Strength is
list position: among the predecessor’s rotation_keys for maintenance
events, among its revealed next_rotation preimages for rotation events,
strongest first. The displaced branch is nullified: its entries remain
in the log, and verifiers MUST ignore them when walking the chain.
Nullified entries do not count toward the 256-event bound of
§ 4.4.4 Verifying a Chain (Offline), which is over the accepted chain.
The window is what turns a stolen rotation key from a seizure into a race the owner can win late: a thief’s event, even registered first, stands only if no stronger key answers within the window. Past the window, registration is final. The window is complementary to pre-rotation (§ 4.4.2 Rotation Keys and Pre-Rotation): pre-rotation bounds which keys can ever authorize an event, and the window resolves races among the keys that can.
When a maintenance event and a rotation event conflict under one
predecessor, their signers are ranked in different lists. Define the
cross-class comparison (proposal: a revealed next_rotation preimage
outranks every current rotation key, since pre-committed keys are the
colder set) before 1.0.
The signed, replicated log head makes registration order tamper-evident (§ 5.3.11 Identity Log): an attacker cannot insert an earlier entry or reorder existing ones without forking a head that mirrors already hold.
4.4.6. The Home Host as Registrar
The home_host field commits the identity to its home host, and with it
the capacity this document calls the registrar: the host, ordering the
identity’s log by accepting registrations, assigning their positions, and
signing the head that attests them (§ 5.3.11 Identity Log, Appendix B. Glossary). The
commitment is what makes "which log is this identity’s" a fact in the
chain rather than a claim to adjudicate: a rival host serving a competing
history either presents a signed event naming itself home_host, or it
presents nothing. An identity changes home host by a key event registered
in the old host’s log, which is the same handoff the log itself makes
(§ 7.9 Holder Host Migration, fondachi/log-continuation); where the old host
is gone, the continuation rules there apply to the identity’s events as
to every other entry.
A registrar SHOULD rate-limit key-event registrations per identity: key events are rare by design (§ 4.4.4 Verifying a Chain (Offline)), and registering one at all takes a validly signed event, so sustained volume means a stolen rotation key filling the log. The limit MUST NOT refuse an event that displaces one whose recovery window is open (§ 4.4.5 Registration, Conflicts, and the Recovery Window): a limit a thief can fill would otherwise lock out the owner’s answer.
A registrar that equivocates, signing two heads that contradict each other, has manufactured the proof of its own misbehavior: the two signed heads are a self-contained certificate, and peers SHOULD refuse to peer with a registrar so caught (§ 9.5 Trust). Reputation never has to adjudicate which history is true; the commitments and signatures settle that.
An identity whose last accepted event names no home_host has no
registrar: its events verify offline but lose conflicts
(§ 4.4.5 Registration, Conflicts, and the Recovery Window), the same standing as a holder with no home
host (§ 7.9 Holder Host Migration).
4.4.7. What This Buys
Note: This section is non-normative.
Key events compose with the at-issuance rule of § 4.2 Keys and Signatures. Grants issued before a rotation stay valid forever, verified against the signing key of their moment; a rotation only points verifiers at the new key for new signatures. Nothing about the grant moves, because the DID a grant names never changes: rotation, recovery, and inheritance all happen under the identifier, and the library stays where it always was.
It also bounds what a hot-key compromise can do. A thief holding the signing key can mint grants until the owner rotates away (a window the recovery machinery closes), and each such grant is revocable by settlement reversal (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)). But the thief cannot sign a key event, because that takes a rotation key; cannot rotate the rotation set, because that takes a pre-committed key; and cannot stand up a rival history, because no other genesis hashes to the DID.
The result is a stable identifier, a recovery window, and prioritized cold keys, with no central registry. The ordering authority is the identity’s own registrar, chosen in the chain and changeable by the same mechanism as everything else, and every event verifies offline from the presented bytes.
4.5. External Identity Bindings
External identity bindings are an optional extension. Grants always name the DID; these bindings, and the domain anchor that drives a verified creator badge, are login, display, and anti-impersonation conveniences. They are never part of the license or retrieval path.
This is also the answer to a fair question: why define an identity root at
all rather than build on atproto or ActivityPub? Neither can carry the
ownership promise of § 4.1 Identifiers. A did:web drags a domain fetch
into every verification and dies with the domain. atproto’s did:plc is
the nearest relative, and its operation chains verify offline much as key
events do, but its ordering authority is one global directory with no
exit: every contested recovery is refereed, every key event registered, and
every identity enumerated by a single party that cannot be migrated away
from, and whose availability, policy, and jurisdiction would sit under the
root of every license forever [ATPROTO]. This protocol keeps the same
recovery semantics and gives the ordering role to the identity’s own
registrar, which is chosen and changeable (§ 4.4.6 The Home Host as Registrar). An ActivityPub
actor is a URL on a server, with no portable root at all [ACTIVITYPUB].
So both ecosystems attach here, for login, reach, and badges, and neither
replaces the self-certifying root.
4.5.1. atproto
An atproto account (for example, a Bluesky handle) binds to a Fondachi root like any other external identity: the host verifies control of the atproto identity at link time (an atproto OAuth session or a key proof), and the DID’s profile (§ 4.6 Profile Document) lists the bound handle. This drives "sign in with Bluesky" and a verified-handle badge. The atproto identity never holds licenses; grants always name the Ed25519 root.
4.5.2. ActivityPub
An ActivityPub actor has no portable root, so its binding is a bidirectional proof:
-
The actor publishes an identity proof in the style of FEP-c390 [FEP-C390], containing the Fondachi DID and signed by that DID’s key.
-
The DID’s profile (§ 4.6 Profile Document) lists the actor URL.
Verifiers MUST require both directions before displaying a binding as verified. A one-way claim in either direction is an impersonation vector: the actor claiming a DID it does not control, or a DID claiming an actor that never consented.
4.5.3. Domain Anchor
A domain vouches for a creator’s DID by listing it at
https://<domain>/.well-known/fondachi-creators, one DID per line
(§ 11.1 Well-Known URIs). A host or verifier that fetches the list and
confirms the DID is present MAY show a verified-domain badge ("✓
example.band"), and SHOULD re-fetch periodically rather than trust a stale
result.
This is what gives a creator a recognizable domain identity without the domain being the license root: if the domain lapses or changes hands, the badge disappears, and every license still verifies against the unchanged DID. The anchor is a display and trust signal, never part of the license or retrieval path.
4.6. Profile Document
The profile document is an optional extension. An identity MAY publish a profile as a signed object (§ 5.2 Signature Envelope) carrying display metadata: a display name, an avatar blob CID, bound external identities (§ 4.5 External Identity Bindings), a home host endpoint (Appendix B. Glossary), and, for a creator, the catalogs they publish. Hosts serve profiles for their users.
A profile is advisory metadata for display and discovery. It is never a trust root: nothing in a grant chain, a key-event walk, or a retrieval decision may depend on it.
The profile document has no specified body; fondachi/profile is
reserved (§ 11.2 Object Type Identifiers). Specify the body or drop this section
before 1.0.
5. Canonical Encoding and Object Model
5.1. Canonical Encoding
Signed objects are encoded as DAG-CBOR: deterministic CBOR as specified by IPLD [DAG-CBOR]. Map keys use the canonical ordering, length first and then bytewise. Signatures are always computed over the DAG-CBOR bytes of the object, so two implementations that disagree about encoding disagree about identity, and the encoding vectors of Appendix A. Test Vectors pin the exact bytes.
Object references are CIDv1 [CID] with the sha2-256 multihash: codec
dag-cbor for objects, codec raw for content chunks and blobs. Where this
document says "the hash of X" or "X’s ID", it means this CID. The registered
parameters are collected in § 11.6 Algorithm Identifiers and Parameters.
Timestamps are integer UNIX seconds, UTC (§ 3.1 Notation).
On the wire, a signed object travels as its canonical DAG-CBOR bytes; the HTTP API MAY additionally expose DAG-JSON views for ergonomics, which are non-authoritative and governed by the rules of § 8 HTTP API.
5.2. Signature Envelope
Every signed object is transported as an envelope:
envelope = { type : tstr, ; e.g. "fondachi/license-grant" ver : uint, ; object schema version, starts at 1 body : { * tstr=> any}, ; type-specific map, schema per object below sigs : [ + { signer : did , ; MUST be a did:key alg : "ed25519" , ? delegation : cid , ; present when signing under a Delegation sig : bstr, ; over the DAG-CBOR of {type, ver, body} }], }
The signature is computed over the DAG-CBOR encoding of {type, ver, body},
so the type and version are covered and cannot be swapped under a valid
signature. ed25519 is the only registered alg value
(§ 4.2 Keys and Signatures, § 11.6 Algorithm Identifiers and Parameters). Signature
validity is evaluated at issuance time, per the rule in
§ 4.2 Keys and Signatures.
signer MUST be a did:key (§ 4.1 Identifiers); a verifier MUST
reject a signature whose signer uses any other method. The verification key
is therefore always the signer itself, and there is no key reference to
resolve. Whether that key speaks for an identity is the separate,
offline check of § 4.4.4 Verifying a Chain (Offline): an object attributed to a
did:fnd is validly signed when its signer was that identity’s
signing key at issuance.
Unknown fields in body MUST be preserved and MUST be included in signature
computation. This is the forward-compatibility rule: an implementation that
strips fields it does not understand breaks the signatures of newer peers.
The unknown-fields group (§ 3.1 Notation) marks this extension point in
every schema within a body; the envelope’s own top level and its signature
entries are deliberately closed, since the rule covers body alone.
Note: This envelope is Fondachi’s own rather than COSE or JOSE, and the
reasons are recorded here. The signing bytes must be
the same deterministic DAG-CBOR the protocol already requires for content
addressing, so COSE would introduce a second canonical form and a
CBOR-inside-CBOR double encoding with no gain; the per-signature delegation
reference has no standard COSE or JOSE header; and with exactly one
registered algorithm, the algorithm-agility machinery those envelopes exist
to provide is dead weight. JOSE additionally serializes to base64url JSON,
which is not deterministic. What is given up is off-the-shelf tooling, and
the trade was judged worth it: the envelope above is small enough that the
signing vectors specify it completely.
5.3. Protocol Objects
Each object carries a fondachi/<type> tag; the full registry is in
§ 11.2 Object Type Identifiers. Catalog objects (manifests, works,
releases, streams and their entries, offers, delegations, takedowns, mirror
authorizations) are public: they are served at /fondachi/object/{cid}
(§ 8.2 Catalog (Host Role)), and only the content bytes they point at are
encrypted (§ 6.1 What Is Encrypted). Grants and revocations are not
served publicly; see the privacy rules in § 5.3.11 Identity Log.
Three object types are specified where they are used rather than here: KeyEvent in § 4.4 Key Events (Rotation and Recovery), CreatorRail in § 8.6 Rails, and SealingKeyDeclaration in § 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration).
5.3.1. ContentManifest (fondachi/manifest)
The signed record of one revision of a work: its position in the work’s sequence, and the bytes that occupy it.
manifest-body = { work : cid , rev : uint.ge 1 , ; 1-based position in the work's revision sequence ? notes : tstr, ; changelog files : [ + { path : tstr, size : uint, chunk_size : ( uint.ge 1024 ) .le 4194304 , ; a power of two cipher_chunks : [ + cid ], ; ciphertext chunks: what mirrors hold and serve unknown-fields , }], enc : { alg : "xchacha20poly1305" , wrapped_key : bstr, ; K_work, HPKE-sealed to the creator's sealing key unknown-fields , }, unknown-fields , }
A manifest binds itself to its work twice over: work is signed into the
body, and the ciphertext it references is encrypted under a key derived from
the work’s CID (§ 6.4.2 Per-Work Content Keys), so a manifest cannot be
reused across works even in principle. A single root-signed manifest is
therefore sufficient on its own to establish which work it belongs to and
its position in that work’s sequence.
rev is 1-based, and a manifest with rev of 0 is invalid. This is how
editions are counted (a work at revision n has had n revisions, and the
numbers are user-visible), and it keeps zero free as a tripwire: rev is an
unsigned integer, a forgotten field default-initializes to 0 in most
languages, and 1-based numbering turns that bug into a self-evidently
invalid record instead of a plausible first revision.
Each (work, rev) position is occupied by exactly one manifest, the
first-registered one winning as tiebreaker; the publish flow requires hosts
to refuse a second at append time (§ 7.1 Publish). Two manifests
signing the same (work, rev) are self-evident creator equivocation. Completeness ("do I have every revision?") and latest-ness are
catalog properties, answered by the live identity log; no signed record can
prove them, since a withholding host could always hide later records.
Chunks are raw blobs addressed by CID (raw codec). chunk_size MUST be a
power of two between 1 KiB and 4 MiB, declared per file, and SHOULD be at
least 256 KiB for real content; the small sizes exist so test vectors stay
reviewable.
Each cipher_chunks[i] addresses the ciphertext that mirrors hold and serve
for chunk i, which is the plaintext chunk sealed under the work’s content
key with a derived nonce prepended. The nonce is synthetic: it is derived
with HKDF, keyed by the content key, over the plaintext chunk’s CID. The
derivation borrows the idea of SIV-mode encryption [RFC5297], while the
authenticated encryption (AEAD) itself is unchanged and decryption still
verifies each chunk’s authentication tag, failing on any tampering. Because
the nonce is a pure function of the key and the plaintext, encryption is
deterministic: identical plaintext within a work converges, uploads
deduplicate, and a revision re-stores only its changed chunks. The derived
nonce also makes the signed cipher CID a commitment to the plaintext,
because the nonce is inside the bytes the cipher CID addresses; the AEAD tag
alone does not provide this commitment, so verifying the nonce after
decryption is a mandatory part of the construction (§ 6.4.4 The Plaintext Commitment). The
construction, the derivation, and its privacy bounds are specified in
§ 6.4.3 Chunk Encryption; the algorithm identifiers are registered
in § 11.6 Algorithm Identifiers and Parameters.
The manifest carries no plaintext CIDs. A plaintext hash in a public document would hand anyone a confirmation oracle for the encrypted content (hash a guessed file, compare), while the keyed nonce commitment gives holders the same integrity without giving outsiders anything.
enc is REQUIRED on every manifest; there is no plaintext form.
wrapped_key is the work’s content key, sealed with HPKE to the public sealing
key the creator has declared (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)); only the
seal boundary can unwrap it, and it is what the sealer unwraps before
re-sealing the key to a grantee. It is safe to publish: a mirror holding it
cannot recover the content key. Holders never read it; they receive keys
sealed to them in the grant (§ 5.3.7 LicenseGrant (fondachi/license-grant)) or by redemption
(§ 6.6 Key Redemption). Each manifest’s wrapped_key is
authoritative for that manifest, so a later revision MAY carry a different
wrap of the same content key, for example one wrapped to a rotated sealing key
(§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)); the content key itself is fixed per
work. The secret the content key derives from
never leaves the publishing client (§ 6.4.1 The Content Master Secret).
A manifest describes files and nothing above them; it is an integrity
commitment rather than a presentation document. The protocol does not define
rendition or format taxonomy. A creator publishing several formats ships
them as files with meaningful paths (FLAC/, V0/, an HLS index for
streaming), exactly as DRM-free downloads are organized today, and a client
presents the file tree and fetches the subset its user selects; any richer
structure is a convention inside the payload, free to evolve without a
protocol change. Everything a manifest points at is encrypted; preview
material, which must be readable without a grant, is outside the protocol
entirely (§ 5.3.3 Release (fondachi/release)).
Companion material (liner notes, documentation, a durable capture of a work’s companion page) likewise has no field of its own: it is ordinary licensed content, shipped as payload files like owned artwork (§ 5.3.3 Release (fondachi/release)). A creator who wants it priced separately publishes it as its own work.
5.3.2. Work (fondachi/work)
A stable lineage identity for a single piece of content:
work-body = { creator : did , slug : tstr, created_at : timestamp , unknown-fields }
The work’s ID is the CID of this object; manifests reference it. A work carries no title, media type, or content kind. How it is presented (album, video, book) is decided by releases and storefronts, not by signed lineage.
Granularity is the creator’s choice. The work is the unit of ownership and revision, so the choice determines what is bought whole and what a revision replaces. A creator who wants to sell a part of a work separately publishes that part as its own work, in its own release.
5.3.3. Release (fondachi/release)
Public catalog metadata naming one or more works, in order: the durable identity of what is sold.
release-body = { title : tstr, ? description : tstr, works : [ + cid ], ; ordered unknown-fields , }
A release is what a purchase offer prices (§ 5.3.5 Offer (fondachi/offer)), and a work MAY appear
in more than one release. Everything in a release is public by the boundary in
§ 6.1 What Is Encrypted, and everything in it is inline text: the
release references no blobs.
The core release carries no artwork or preview material. Pre-purchase presentation is a host matter (§ 1.3 Scope of This Document): a creator supplies covers and marketing samples to hosts by whatever arrangement suits them, as ordinary public web assets rather than licensed content, and a creator who wants their public artwork standardized across the federation publishes it through the optional ReleaseAnnotations extension (§ 5.3.12 ReleaseAnnotations (fondachi/release-annotations), Optional Extension). The artwork a holder owns travels inside the licensed artifact itself, as ordinary files in the manifest (§ 5.3.1 ContentManifest (fondachi/manifest)), so a holder who has their content has their covers, and a fresh device retrieves them like any other chunks.
Note: By convention, a work’s artwork ships in that work’s payload, and
clients look for a cover.* file at the payload root.
The release is deliberately this small. It is the durable identity of what
is sold, embedded in every grant issued under it (§ 5.3.7 LicenseGrant (fondachi/license-grant)), so it carries only
what should never change under a buyer. Amendable, discovery-facing metadata
(credits, tags, current listing text) lives in ReleaseAnnotations (§ 5.3.12 ReleaseAnnotations (fondachi/release-annotations), Optional Extension),
where it can be corrected without re-signing a release whose CID offers name
and grants embed. description sits in both, playing a different role in
each: here it is the text the subject was sold as, and an annotation carries
the text it is listed as now.
5.3.4. Stream (fondachi/stream) and StreamEntry (fondachi/stream-entry)
A stream is a stable identity for a sequence of releases a creator publishes over time, and it is what the stream offer models price (§ 5.3.5 Offer (fondachi/offer)). A stream may cover the whole catalog or any slice of it: a supporters feed, a series, a podcast. Like a work, it is an identity, not a container:
stream-body = { creator : did , slug : tstr, created_at : timestamp , unknown-fields }
Releases join a stream by creator-signed entries:
stream-entry-body = { stream : cid , release : cid , issued_at : timestamp , unknown-fields , }
A StreamEntry MUST be signed by the creator’s signing key, never by a delegated key, for the same reason as offers (§ 5.3.5 Offer (fondachi/offer)): membership is price-relevant. A stream offer’s price covers whatever lands in its streams, so a delegate that could add releases to a stream could underprice the catalog one stream at a time.
An entry is registered by appending it to the creator’s identity log (§ 5.3.11 Identity Log), and registration is the event that the rest of the protocol keys on: a release is "in" a stream from its entry’s registration, and subscription accrual and the sealer’s coverage check both key on registration time (§ 7.5 Subscription Lifecycle, § 10.7 The Active-Custodian Problem (Operator Forgery)). A release MAY belong to any number of streams, and an entry MAY be registered for a release published long before the stream existed. Entries are additive in this version; removal is not specified.
Removing a release from a stream, which would affect future accrual only, is not specified in this version.
5.3.5. Offer (fondachi/offer)
A signed statement of the terms on which a subject may be licensed:
offer-body = { subject : [ + cid ], ; releases; for the stream models: streams model : "purchase" / "stream-future-only" / "stream-complete" , price : { currency : tstr, ; ISO 4217 alphabetic code ( amount : uint; fixed price, in the currency's minor units // pwyw_floor : uint), ; pay-what-you-want minimum, same units, never 0 unknown-fields , }, valid : { ? from : timestamp , ? until : timestamp , unknown-fields }, unknown-fields , }
The subject rule is per model. For purchase, the subject is one or more
releases: the release is the sellable unit, and selling a single
work is done with a release containing one work. For the two stream models,
stream-future-only and stream-complete, the subject is one or more
streams (§ 5.3.4 Stream (fondachi/stream) and StreamEntry (fondachi/stream-entry)): a stream offer prices a sequence of releases as it grows,
which no fixed release list can express. Works are never offered directly;
every grant therefore traces back to the release it was sold under (§ 5.3.7 LicenseGrant (fondachi/license-grant)).
A subject MAY be under any number of concurrent offers. Which buyers a host lets transact against which offer is host commerce, outside this document; what the protocol verifies is that the grant’s provenance names a creator-signed offer whose price the settlement satisfies (§ 10.7 The Active-Custodian Problem (Operator Forgery)).
An offer MUST be signed by the creator’s signing key (§ 4.2 Keys and Signatures), never by a delegated sales key. The offer is the price anchor for settlement binding: the sealer releases keys only against a settlement satisfying a creator-signed price, and a delegated key is typically held by the very party that binding constrains (§ 10.7 The Active-Custodian Problem (Operator Forgery)).
price names exactly one of amount, a fixed price, and pwyw_floor, a
pay-what-you-want minimum below which the buyer may not name an amount.
Both are integers in the minor units of currency, an ISO 4217 alphabetic
currency code [ISO4217]. A price of zero is amount: 0: the creator
declaring the subject free, which waives settlement verification for it.
pwyw_floor MUST be nonzero, so that only an explicit amount: 0 waives
settlement.
There is no rights field. Access to the subject’s bytes is intrinsic to what a sale conveys, and everything a work includes (artwork, companion material) is conveyed with it; a creator who wants to sell less publishes less, as a separate work. There is deliberately no stream-only, no-download, or per-file right: a client decrypts locally, so any such restriction would be unenforceable without the DRM this protocol renounces (§ 1.2.2 Non-Goals). A future revision that finds a genuinely separable right can add an optional field without breaking signatures (§ 5.2 Signature Envelope).
A stream offer is a standing order: while the buyer’s recurring settlement
is active, each entry that lands in the offer’s streams is bought, and the
resulting grant is as permanent as any purchase. The two models differ only
in what the buyer’s first settlement under the offer conveys. Under
stream-future-only, it conveys nothing already in the stream. Under
stream-complete, the first settlement additionally funds grants for every
release in the stream at that moment; this is the natural shape for a season
of a periodical, where a new subscriber receives the season so far at once
and accrues the rest as it lands. Under either model there is no lapse that
takes anything back
(§ 7.5 Subscription Lifecycle), and a stream-complete price knowingly sets the
one-payment cost of everything the stream contains
(§ 10.1 Threat Model).
5.3.6. Delegation (fondachi/delegation) and Revocation
A narrow, signed authorization from a creator to another DID, typically a host:
delegation-body = { delegator : did , ; creator root delegate : did , ; e.g. a host's sales key scope : [ + ( "sell" / "publish" / "takedown" )], valid : { from : timestamp , until : timestamp , unknown-fields }, unknown-fields , }
A delegation always covers the delegator’s whole catalog; there is only
one per identity (§ 5.3.11 Identity Log), so the delegator names it, and scope narrows
by right, not by subject. The sell right authorizes executing sales
against the creator’s identity-signed offers: accepting settlement, issuing
the resulting grants, and accruing subscription grants per registered
stream entries. It does not authorize creating or altering offers or stream
entries; price is not delegable by construction (§ 5.3.5 Offer (fondachi/offer), § 5.3.4 Stream (fondachi/stream) and StreamEntry (fondachi/stream-entry)). publish
authorizes appending catalog objects the delegate may sign; takedown
authorizes the actions of § 5.3.9 Takedown / Delist (fondachi/takedown).
A delegation is withdrawn by a revocation, signed by the delegator:
delegation-revocation-body = { delegation : cid , effective_at : timestamp , ; MAY be later than issued_at, announcing a wind-down issued_at : timestamp , unknown-fields , }
Objects signed under a delegation are valid iff the delegation was unrevoked and within its validity at issuance; revocation is never retroactive (§ 4.2 Keys and Signatures).
No object this document specifies is signed by a delegate under
the publish right: manifests, works, releases, offers, stream entries,
sealing-key declarations, and rails are all root-signed. Either enumerate
what a delegate may sign under it, or remove it and state that a
registrar appends an identity’s own signed objects to its log without
any delegation, which also resolves the bootstrap question of how a
delegation is itself appended.
5.3.7. LicenseGrant (fondachi/license-grant)
The unit of ownership:
license-grant-body = { id : bstr.size 16 , ; random; keeps the CID a hiding commitment grantee : did , subject : [ + cid ], ; works release : envelope , ; the creator-signed Release this was sold under provenance : { offer : cid , ? settlement : settlement-ref , ; omitted only under a zero-price offer unknown-fields , }, legal : "fondachi-sl-1" , ; Standard License Terms version id sealed_keys : [ + { manifest : cid , alg : "hpke-x25519-sha256-chacha20poly1305" , ct : bstr, unknown-fields , }], issued_at : timestamp , unknown-fields , } settlement-ref = any; rail-scoped payment reference, shape rail-specific
settlement-ref is typed any because settlement evidence is
rail-specific. Per-rail profiles pinning its shape are pending
(§ 8.6 Rails).
A grant is signed by the sales key under a live sell delegation, or by the
creator root directly. provenance.settlement is REQUIRED whenever the
named offer carries a nonzero price: a paid grant without provable
settlement is auditable as over-issuance, and settlement reversal is the
only path to revocation (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)). Under a zero-price offer the field is
absent, and a verifier MUST NOT accept a grant lacking a settlement unless
the offer it names is creator-signed with a price of zero (§ 5.3.5 Offer (fondachi/offer)). Having no
settlement, such a grant has no reversal path and is irrevocable. legal
binds the grant to the
versioned Standard License Terms, under which personal use is permitted and
commercial resale is not licensed.
A grant chain MUST be verifiable offline: the grant, the delegation it was issued under, the creator root, and the key events of any identity in the chain whose keys have rotated, together with the subject’s root-signed manifests, suffice without the participation of any live server. This is what lets any mirror serve a holder without consulting the issuing host, and what keeps ownership provable after that host is gone.
The embedded release. release carries the complete creator-signed
release envelope the grant was sold under, so a holder can see what they
bought (title, ordering, description) without consulting any server, and so
that presentation cannot be fabricated by the host: the grant
is host-signed, but the embedded release is creator-signed; checking that
every granted work appears in the embedded release is part of verifying a
purchase (§ 7.2 Purchase). The embedded
release is a snapshot of what was sold: a later re-signed release does not
change what the holder bought. The subject is unchanged by the embedding: a
grant’s subject is works, never a release. The release is carried as
evidence of what was sold, not as what is owned.
A grant conveys its works whole: it covers every revision, past and future.
Revisions update what was bought and are never a new sale; a creator who
wants to sell a new version separately publishes it as its own work (§ 5.3.2 Work (fondachi/work)).
Verifying that a manifest falls under a grant is a single offline check:
the manifest’s signed work is among the grant’s subject works (§ 5.3.1 ContentManifest (fondachi/manifest)).
What was delivered at issuance stays on the record regardless, because
sealed_keys names the manifests sealed at that moment. No further key is
needed for other revisions: the content key is fixed per work, so the key
delivered at issuance decrypts every revision, and redemption exists for
lost keys and succeeded holders (§ 6.6 Key Redemption).
One grant per release. A grant embeds exactly one release, and its
subject lists that release’s works. An offer over several releases yields
several grants, and a stream offer issues one grant per release as each
entry lands (§ 5.3.4 Stream (fondachi/stream) and StreamEntry (fondachi/stream-entry)). Grants from one purchase share their
provenance.offer and provenance.settlement, which is how they are grouped
and how a settlement reversal revokes exactly that set; the purchase flow
issues the set atomically (§ 7.2 Purchase). This granularity maps
one-to-one onto the releases a library displays.
Every grant is perpetual. There is no terms field, no expiry, and no conditional form: a grant, once issued, conveys what it conveys until a reversal of its settlement revokes it (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)). Rental and expiring subscription modes were considered and rejected, because expiry could only ever bound continued service, never access to what a holder has already retrieved and decrypted; a time-boxed grant would promise an enforcement this protocol cannot and does not provide (§ 1.2.2 Non-Goals).
The holder. A grant’s holder is its grantee, forever: the DID never
changes, and what rotates beneath it is the signing key that acts for it
(§ 4.4 Key Events (Rotation and Recovery)). A grant changes hands only when the identity itself
does: an heir, holding a rotation key, takes over the whole identity and
everything it holds (§ 7.6 Inheritance (Takeover)). This is the only conveyance:
there is no per-grant transfer, and no consolidation of one identity’s
grants into another; and because a takeover moves the whole identity,
library and all, it is not a gifting mechanism. The Standard License Terms
do not license resale, and a conformant implementation MUST NOT provide
resale facilitation: listing, price discovery, escrow, or marketplace
features. An heir holds the grant but not the keys sealed to it, which
remain sealed to the signing key of the moment of sale; they obtain keys
sealed to their own key by redemption (§ 6.6 Key Redemption,
§ 7.6 Inheritance (Takeover)). Revocation follows the settlement, not the holder:
a reversal of the original purchase’s settlement revokes the grant in the
heir’s hands (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)).
Sealed keys. sealed_keys is REQUIRED. Each entry is the work’s content
key sealed to the grantee with HPKE, one entry per manifest current at
issuance. Grantee keys are Ed25519 and are converted to X25519 for sealing,
so a single HPKE suite serves every grantee and no second curve enters the
protocol (§ 6.4.5 Key Wrapping,
§ 11.6 Algorithm Identifiers and Parameters).
Storage representation. A grant is transmitted and verified with the
release embedded. An implementation MAY store it in any representation that
reproduces the identical signed bytes; because the encoding is deterministic
(§ 5.1 Canonical Encoding) and the release envelope is itself content-addressed, a store can hold
one copy of a popular release keyed by CID and reassemble grants on demand,
byte for byte. The duplication this addresses lands on servers, which keep a
grant per sale; holders are unaffected. sealed_keys does not deduplicate
this way, because it is per-grantee by construction.
5.3.8. GrantRevocation (fondachi/grant-revocation)
grant-revocation-body = { grant : cid , cause : { settlement_reversal : settlement-ref , unknown-fields }, issued_at : timestamp , unknown-fields , }
A revocation is signed as the grant it revokes was: by the sales key under
a live sell delegation, or by the creator root. The only valid cause is a
reversal of the settlement named in the grant’s provenance. Verifiers MUST treat a revocation with any other cause as an
invalid object, not as a revocation. A
reversal revokes every grant issued under the reversed settlement, which for
a multi-release purchase is exactly the set sharing that
provenance.settlement (§ 5.3.7 LicenseGrant (fondachi/license-grant)). A grant issued under a zero-price offer
names no settlement and therefore cannot be revoked at all (§ 5.3.7 LicenseGrant (fondachi/license-grant)).
A revocation is registered in the issuing creator’s identity log by an
entry of kind revocation naming the revoked grant (§ 5.3.11 Identity Log);
from that entry’s visibility, every mirror of the catalog refuses
retrieval proofs for the grant (§ 7.4 Retrieval). The body itself is not
served publicly.
5.3.9. Takedown / Delist (fondachi/takedown)
takedown-body = { subject : [ + cid ], action : "delist" / "takedown" / "relist" , reason : "creator-request" / "dmca" / "illegal" / "host-policy" , issued_at : timestamp , unknown-fields , }
Signed by the creator root, or by a key holding a live delegation with
takedown scope; live, so a host whose delegation has lapsed cannot grief
a catalog it no longer serves. The two actions differ in force. delist stops sales
and nothing else: it MUST NOT prevent an existing holder from retrieving
what they already hold, and relist reverses it (valid only against a
current delist). takedown stops sales and
obliges every mirror to stop serving the subject’s bytes once it is visible
to them, and it is terminal: taken-down content is re-published rather than
re-listed. Neither action deletes
grants; ownership records survive the bytes, and a holder’s local copy is
beyond the protocol’s reach entirely. Propagation is specified in
§ 7.10 Takedown Propagation.
The authorization requirement gates only the power this object grants: to
bind operators other than the signer, network-wide, in the creator’s name.
It does not condition legal compliance. Any operator MAY stop serving or
selling anything on its own infrastructure at any time, with no object
signed and no authority from anyone; the protocol cannot compel service,
and an operator’s obligations under a lawful takedown notice attach to that
operator directly, outside this document. A host holding a takedown
delegation complies network-wide through this object (reason dmca or
illegal); the propagation, and the case where no delegation covers the
content, are specified with the flow (§ 7.10 Takedown Propagation).
The type of subject CIDs is unspecified (releases, works, or
manifests); mirrors must map it to bytes and hosts to offers.
5.3.10. MirrorAuthorization (fondachi/mirror-auth)
mirror-auth-body = { mirror : did , ; the mirror's service key valid : { from : timestamp , until : timestamp , unknown-fields }, unknown-fields , }
Signed by the creator chain, and covering the creator’s entire catalog: this is what makes a mirror an authorized server of the catalog’s ciphertext. There is no narrower scope, deliberately. Authorization is permission, not obligation; a mirror chooses what it actually stores, so a scope field would exist only to forbid serving parts of a catalog to a mirror trusted with the rest, and no need for that has been demonstrated.
One exception needs no authorization: a mirror MAY hold and serve bytes for a specific holder on presentation of that holder’s valid grant chain, serving only that holder. This is what allows personal and household mirrors without creator involvement.
Authorization and location are deliberately separate. A MirrorAuthorization names a service key and is durable, signed catalog data; where that mirror is reachable is operational fact, published in discovery documents (§ 8.1 Discovery, § 9 Federation) rather than in any signed object; the retrieval flow verifies a candidate endpoint against a live authorization (§ 7.4 Retrieval). Nothing signed therefore goes stale when a mirror moves, and no offer or release carries a server address.
5.3.11. Identity Log
Every identity has exactly one identity log: an append-only, hash-linked log of the signed objects published under its authority. There is one authoritative chain per identity, not one per host; the copies peers hold by gossip are non-authoritative caches (§ 1.3 Scope of This Document, § 9 Federation).
For a creator, the log carries the catalog objects (works, manifests, releases, streams and stream entries, offers, takedowns), its key events (§ 4.4 Key Events (Rotation and Recovery)), and the revocations of grants issued under it, registered so that every mirror of the catalog learns which grants to refuse (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)). Grants themselves are not registered: they are held by the parties to the sale (§ 5.3.7 LicenseGrant (fondachi/license-grant)). For a holder, the log carries only its key events; it is ordinarily served by the holder’s home host, which acts as registrar (§ 4.4.6 The Home Host as Registrar, § 7.9 Holder Host Migration). For creator and holder alike, the host serving an identity’s log is that identity’s home host (Appendix B. Glossary).
Structure. A log is a chain of entries, each registering one thing:
log-entry = { prev : cid / null, ; the preceding entry; null only at seq 1 seq : uint.ge 1 , ; 1-based position in the log registered_at : timestamp , ; asserted by the registrar kind : "object" / "revocation" , object : cid , ; the registered envelope, or the revocation ? of : cid , ; kind revocation only: the revoked grant }
An entry of kind object registers a published envelope, and the log
serves that object by its CID; an entry of kind revocation registers a
GrantRevocation by CID alone, under the privacy rule below, and MUST name
the revoked grant in of, which is what a mirror matches a presented
grant against (§ 7.4 Retrieval).
The head of a log is the CID of its latest entry. Because each entry
names its predecessor by CID, the head commits to every entry before it,
and because the registered objects are content-addressed, a peer holding
the entries and the published objects holds the log exactly; entries are
plain DAG-CBOR maps, not envelopes, and carry no signature of their own.
Where this document
says an object is registered at a log position, or that one registration
precedes another, it means seq; where it says a registration time, it
means registered_at, which the registrar asserts and the host’s signed
head stands behind. The log-entry map is closed, like the RetrievalProof:
it is not a body, and a new kind of entry is a revision of this document.
The serving host attests the head by signing a LogHead
(fondachi/log-head) with its service key:
log-head-body = { identity : did , ; whose log head : cid , ; the latest log-entry length : uint, ; the head entry's seq signed_at : timestamp , unknown-fields , } ; signed by the serving host's service key
The LogHead is served at a fixed endpoint (§ 8.2 Catalog (Host Role)); followers
poll it. It is an envelope that never enters a log: it is the host’s
statement about the log, not part of it. Two LogHeads from one service
key naming the same identity and length but different head values
are proof of equivocation (serving different log views to different
parties), attributable to a named operator.
On migration the log moves whole: the new serving host imports every existing entry and continues the same chain (§ 7.8 Creator Host Migration, § 7.9 Holder Host Migration). The handoff itself is marked by a LogContinuation:
log-continuation-body = { identity : did , ; the log's identity; the signer MUST be its ; signing key at issuance ([[#key-event-walk]]) prev_head : cid , ; the last log-entry at the previous serving host issued_at : timestamp , unknown-fields , }
The identity signs it naming the head its log had reached at the previous
host, and the migration flows place it as the first entry the new host
registers (§ 7.8 Creator Host Migration, § 7.9 Holder Host Migration): the entry
registering the continuation has prev equal to prev_head, so the
chain is unbroken across the move. The record does two jobs. It is the
identity’s authorization for the new host to serve the log and act as its
registrar, and it pins the log’s full history at the moment of the move,
so that neither host can quietly drop the tail during the handoff: the
signed heads make equivocation attributable, and the continuation makes
truncation detectable.
There is no other way to move a log, and in particular an identity cannot abandon its log and begin a new one. A verifier or mirror that knows part of an identity’s history and is presented with a chain that does not link back through it MUST keep the history it knows and treat the conflicting chain as a fork, which is evidence of equivocation, never as a replacement. The genesis anchor of § 4.4.1 The Genesis Event closes half of this by construction (no rival log can even open, because no other first entry hashes to the DID) and the rule here closes the rest: the tail of the one log an identity has is as append-only as its head, whoever serves it.
Privacy. Grants are not registered, and a revocation’s entry carries
two CIDs, the revocation’s and the revoked grant’s. A grant body carries a
random 128-bit id, so its CID is a hiding commitment: the entry reveals
that some grant was revoked and nothing about whose, unless you already
hold the object. Grant and revocation bodies are never served publicly
(§ 8.2 Catalog (Host Role)); they are available to the grantee, whoever now holds
its keys, and to the creator for their own catalog.
5.3.12. ReleaseAnnotations (fondachi/release-annotations), Optional Extension
ReleaseAnnotations is an optional extension carrying the amendable, public-facing metadata of a release: credits, tags, the current listing text, and references to public artwork. It exists for indexers. Independent discovery services crawl signed catalogs across the federation, and this is the metadata they run on: a federation whose catalogs carry credits and tags signed lets anyone build search over the network without scraping storefronts, which is part of this protocol’s answer to centralized discovery (§ 1.1 Motivation). Credits embedded in the licensed content would be ciphertext, invisible to every indexer. A conformant implementation MAY omit this extension, and nothing in the core protocol references it.
release-annotations-body = { release : cid , ? description : tstr, ? credits : [ + { role : tstr, name : tstr/ did , ; a display name, or a claimed DID unknown-fields , }], ? tags : [ + tstr], ? artwork : [ + { ? label : tstr, blob : cid , unknown-fields }], issued_at : timestamp , unknown-fields , }
A ReleaseAnnotations is signed by the creator and registered in the creator’s identity log, so it federates, is uniform across mirrors, and survives host migration like any other catalog object. It references the release rather than living inside it, so credits can be corrected and artwork replaced without re-signing a release whose CID offers name and grants embed. Where several ReleaseAnnotations exist for one release, the latest registered one is current and replaces earlier annotations whole; fields are never merged across annotations.
The relationship between the two objects is record of sale versus current
presentation, not a conflict to resolve. Where an annotation carries
description, it is
the release’s current listing text, and presentation surfaces SHOULD prefer
it; the description in the release itself remains what the subject was sold
as, preserved in every grant’s embedded release (§ 5.3.7 LicenseGrant (fondachi/license-grant)). Nothing an
annotation says reaches into any grant already issued.
A credit that names a DID is a claim by the release’s creator, not an attestation by the named identity, and verifying one would require a proof in the other direction (§ 4.5 External Identity Bindings).
The text fields need no special serving: annotations replicate through ordinary catalog sync, so every host carrying the catalog carries them, whether or not it implements the extension. The artwork blobs are what the extension gates: they are public and unencrypted, distinct from the owned artwork inside a work’s payload (§ 5.3.3 Release (fondachi/release)), and a host implementing the extension serves them on its public asset surface and announces that in its discovery document (§ 8.1 Discovery). Labels are free-form; the protocol defines no role taxonomy for them (§ 5.3.1 ContentManifest (fondachi/manifest)).
5.4. Retrieval Proofs
Retrieval proofs are not persistent objects; they are short-lived credentials minted by the holder’s signing key:
retrieval-proof = { grant_chain : [ * cid ], ; the grant, then the grantee's key events since issuance holder : did , audience : did / tstr, ; the one mirror this proof is for (DID or endpoint) scope : [ * cid ], ; manifest CIDs expires_at : timestamp , ; MUST be at most 15 minutes from mint sig : bstr, ; by the holder's current signing key }
sig is an Ed25519 signature over the DAG-CBOR encoding of the proof map
with sig absent, the same rule the envelope applies to its body
(§ 5.2 Signature Envelope); a proof and an envelope have no key in common, so the
bytes one signs can never verify as the other. Where a proof travels
inside a JSON request body (§ 8 HTTP API) it is the DAG-JSON view of this
map, and the verifier re-encodes it to DAG-CBOR before checking sig, as
for any view. A proof serves three roles, and holder names a different
party in each. For retrieval, holder is the grant’s grantee, and sig
is by that identity’s current signing key per the presented key events
(§ 4.4.4 Verifying a Chain (Offline)). For the holder credential of § 8 HTTP API,
holder is the authenticating identity, signed the same way, and
grant_chain and scope are empty. For a mirror session, holder is
the mirror’s service key, both fields are empty, and sig is the service
key’s.
A proof is structurally narrow by design: it authorizes one holder’s retrieval at one mirror, within a named manifest scope, for at most fifteen minutes. What a mirror verifies before honoring a proof, and the per-grant rate limit on honoring them, are specified with the retrieval flow (§ 7.4 Retrieval).
6. Content Encryption
Every work’s content bytes are encrypted: enc is a REQUIRED field of every
ContentManifest (§ 5.3.1 ContentManifest (fondachi/manifest)).
6.1. What Is Encrypted
The boundary runs between content and catalog: licensed content bytes are encrypted, and signed objects are not.
On the encrypted side is every chunk that a manifest’s files reference
(§ 5.3.1 ContentManifest (fondachi/manifest)), which is to say the entirety of what a grant licenses.
On the public side is every published object: works, manifests,
releases, streams and their entries, offers, delegations, mirror
authorizations, takedowns, the identity’s key events, and the sealing-key
declaration (§ 4.4 Key Events (Rotation and Recovery), § 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)). They are served as plaintext at
/fondachi/object/{cid} (§ 8.2 Catalog (Host Role)), and each is inline text
referencing no content blobs, so replicating a catalog reveals metadata and
ciphertext addresses, never content. The manifest itself is public by this
rule, and deliberately safe to publish: its cipher_chunks CIDs address
ciphertext, and its wrapped_key is itself ciphertext that only the seal
boundary can open (§ 6.4.5 Key Wrapping).
Two boundary cases are easy to misread. Grants and revocations are unencrypted like every signed object, but they are withheld from public serving as a privacy rule (§ 5.3.11 Identity Log); that rule protects the holder’s identity, and it is not part of this section’s boundary. Preview material, meaning the covers and samples a storefront shows before any purchase, is outside the protocol entirely (§ 5.3.3 Release (fondachi/release)): it must be readable without a grant, so a creator supplies it to hosts out of band as ordinary public web assets, and the blindness of this section governs licensed content only.
6.2. Gating
Encryption is one of two mechanisms that apply to every retrieval, and each defends against an adversary the other cannot reach. Against the operator, encryption is the defense: an operator stores the ciphertext, so no gate can keep it from what it already holds, and blindness (§ 6.3 The Blind Host Model) is what does. Against everyone else, the gate is the defense, and key secrecy cannot be, because every grantee is legitimately handed the keys: a content key is useless without the ciphertext, and ciphertext is obtained only through the gated retrieval path, under a holder-signed retrieval proof that is bound to one mirror, short-lived, and rate-limited per grant (§ 5.4 Retrieval Proofs, § 8.3 Content (Mirror Role)). Scraping a catalog through the gate costs the same with keys in hand as without them. Goal 3 and Non-Goal 1 in § 1.2 Design Goals and Non-Goals state the same division from the design side.
Because the gate is load-bearing, the retrieval flow is the only path by which content bytes may be served (§ 7.4 Retrieval). Publishing ciphertext openly to gain cheap public storage would reintroduce the mass-leak economics the gate exists to prevent, and being ciphertext does not excuse it.
Neither mechanism substitutes for the other. A deployment that gates plaintext has no defense against its own operator, and a deployment that serves ciphertext ungated has no defense against bulk scraping. Both apply, always.
6.3. The Blind Host Model
This section provides the property that the operator never obtains plaintext or a content key. An operator could otherwise see them at exactly two points, so blindness is enforced at both:
-
Blind at publish. The creator’s client chunks the content, encrypts every chunk, computes the ciphertext CIDs, and signs the manifest; it uploads ciphertext and the signed manifest, and nothing else (§ 7.1 Publish). The host never receives plaintext or a content key. It is not storing blindly on trust: content addressing works on ciphertext, so the publish flow requires the host to verify each stored chunk against the
cipher_chunksCID the signed manifest declares (§ 7.1 Publish). That is integrity without visibility. -
Blind at seal. At purchase and on redemption, the content key is unwrapped and re-sealed to the grantee inside a seal boundary (§ 6.5 Key Release and the Seal Boundary), an environment the operator can neither observe, which would reveal the key, nor alter, which would let it skip policy. A host that unwraps a content key in the clear and seals it itself is not blind, whatever else it encrypts.
Stated positively: plaintext content exists on exactly two kinds of machine, the publishing client that encrypts it and a grantee’s client that decrypts it, both client-side. No protocol service ever holds it. Hosts and mirrors store and serve ciphertext, personal mirrors included (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)), and the seal boundary handles keys, never content (§ 6.5 Key Release and the Seal Boundary). Unlike the key boundary, this one is open on one side by design: what a grantee does with plaintext they have decrypted is beyond the protocol’s reach, since bounding that is what DRM does, and this protocol constrains its services, never its owners (§ 1.2.2 Non-Goals).
The host is thereby reduced to a payment rail, a ciphertext store, and a catalog. Two consequences follow, and one compatibility note.
First, a host cannot derive anything from licensed content: it cannot transcode, repackage, or generate formats, and the protocol accordingly has no rendition taxonomy (§ 5.3.1 ContentManifest (fondachi/manifest)). A host serves exactly the ciphertext it was given, and format variety is the creator’s to supply, as files with meaningful paths inside the payload (§ 5.3.1 ContentManifest (fondachi/manifest)).
Second, encryption at rest is not this property. A host that holds the content key, because it encrypted plaintext it received or because it can extract the private sealing key, can decrypt at will; it defends only against later storage theft, never against the operator itself. That is a weaker and different property, and a deployment MUST NOT present it as the blindness defined here.
The compatibility note concerns grants. A host MAY hold and store grants on a
holder’s behalf, as the origin host’s records and a home host’s library
both do (§ 7.3 Cross-Host (Brokered) Purchase), without weakening blindness: each
sealed_keys entry is ciphertext addressed to the grantee (§ 5.3.7 LicenseGrant (fondachi/license-grant)), distinct
from the manifest’s wrapped_key, which is addressed to the seal boundary
(§ 6.4.5 Key Wrapping). Opening a sealed_keys entry takes the grantee’s own private key,
which no operator holds (§ 4.3 Custody); not even the sealer that
produced it can open it again. A stored grant therefore reveals no key
material to the host storing it. This guarantee rests on private keys being
client-held, which is exactly why § 4.3 Custody prohibits operator custody of
private keys outright: an operator holding a holder’s signing key could
unseal every content key sealed to it.
6.4. Content Keys
Beyond their signing identity (§ 4.2 Keys and Signatures), a creator has one content secret, held only by their publishing client, from which every content key derives. Content keys encrypt and never sign; nothing in this section is usable as, or derivable into, a signing key.
Every derivation in this section is HKDF-SHA256, the HMAC-based key derivation function [RFC5869], used with an empty salt. Each derivation’s info string is a fixed label, registered in § 11.6 Algorithm Identifiers and Parameters, followed by its input where it has one.
6.4.1. The Content Master Secret
content_master is a 32-byte per-creator secret, generated and held on the
creator’s publishing client. It is the derivation root of every per-work
content key, past and future, and through them of every chunk nonce, which
is why its custody rule is absolute: content_master MUST NOT leave the
publishing client, under any custody arrangement, self-custody sealing
included. The seal boundary of § 6.5 Key Release and the Seal Boundary never holds it; it receives each work’s
key wrapped (§ 6.4.5 Key Wrapping), never the root the keys derive from.
content_master MUST be distinct from the identity’s signing key material
and domain-separated from it, so that neither can be used as the other. A
client MAY derive it from the same backup seed as the signing key, which
gives a creator a single secret to back up; the registered derivation for
clients that do is:
content_master = HKDF-SHA256(backup_seed, "fondachi/content-master/v1")
6.4.2. Per-Work Content Keys
Each work has exactly one content key, and a publishing client MUST derive it as:
K_work = HKDF-SHA256(content_master, "fondachi/content-key/v1" || CID(work))
where CID(work) is the binary CID of the Work object (§ 5.3.2 Work (fondachi/work)), || is byte
concatenation, and the output is 32 bytes.
The derivation is deterministic, so nothing is stored per work and a creator
who holds content_master can recover every content key they have ever
used. It is keyed by the work’s CID rather than any manifest’s, so the key
is stable across a work’s revisions, and unchanged chunks keep their
ciphertext from one revision to the next (§ 6.4.3 Chunk Encryption). It also seconds the
manifest’s binding to its work: the ciphertext a manifest references only
decrypts under the key of the work the manifest names (§ 5.3.1 ContentManifest (fondachi/manifest)).
There is deliberately no re-key path: the derivation admits one content key per work, forever. Re-keying a work would promise a recovery it cannot deliver, because the old ciphertext and every key sealed to a grantee are already outside the creator’s reach, and a leaked key is an accepted residual bounded by the gate rather than by key secrecy (§ 6.2 Gating, § 10.1 Threat Model). A creator who wants content under a fresh key publishes it as a new work (§ 5.3.2 Work (fondachi/work)). Losing the private sealing key does not require a new content key either; it requires a re-wrap of the same key (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)).
The derivation binds the publishing client, and no verifier can observe whether it was followed; it is pinned by the test vectors of Appendix A. Test Vectors. What it buys the creator is single-secret recovery, convergent re-encryption, and the re-wrap path of § 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration), all of which fail for a client that substitutes a random key.
6.4.3. Chunk Encryption
Content is chunked per the manifest’s declared chunk_size (§ 5.3.1 ContentManifest (fondachi/manifest)), and
each plaintext chunk is encrypted independently:
N = HKDF-SHA256(K_work, "fondachi/content-nonce/v1" || CID(plaintext)) cipher = N || AEAD-Seal(K_work, N, plaintext)
where CID(plaintext) is the binary CID (raw codec, § 5.1 Canonical Encoding) of the plaintext
chunk, computed locally and never published, and N is 24 bytes, the AEAD’s
full nonce length. The AEAD is XChaCha20-Poly1305 [XCHACHA]: an
authenticated cipher, so decryption fails on any tampering.
The associated data is empty (see below). cipher_chunks[i] in the manifest
is the CID (raw codec) of the i-th cipher value, nonce included, and
that is the byte string mirrors hold and serve.
The nonce is synthetic rather than random, borrowing the idea of SIV-mode
encryption [RFC5297] while leaving the AEAD itself unchanged. Because the
nonce is a pure function of the key and the plaintext, a given (K_work, N)
pair only ever seals one plaintext, so derived nonces reintroduce no reuse
hazard; and encryption becomes deterministic, so identical plaintext under
one K_work yields identical ciphertext. Uploads therefore deduplicate, and
a revision re-stores only the chunks it actually changed.
The privacy bound on that convergence is K_work itself. This is the keyed
form of convergent encryption, analyzed in the message-locked encryption
literature [MLE]: the keyless form lets anyone confirm a guessed plaintext,
while here deriving a nonce or a ciphertext requires the work’s secret key.
An observer can tell which chunks are identical within one work, and can
learn nothing across works or creators, and nothing about plaintext. For the
same reason the manifest carries no plaintext CIDs, which would hand
outsiders a keyless confirmation oracle (§ 5.3.1 ContentManifest (fondachi/manifest)).
The associated data is empty by decision, not omission. Binding a chunk to
its position or its work through the AEAD would duplicate a stronger check:
received bytes are verified against cipher_chunks[i] from the root-signed
manifest before any decryption runs (§ 7.4 Retrieval), which already
fixes both the chunk’s content and its position. A chunk index in the
associated data would also break deduplication of identical chunks within a
work, and a work CID there is redundant with K_work’s scoping.
6.4.4. The Plaintext Commitment
After decrypting a chunk, a client MUST re-derive the nonce, from K_work
and the CID of the just-decrypted bytes, and verify that it matches the
nonce prepended to the ciphertext. A chunk that fails this check MUST be
rejected exactly as a chunk that fails AEAD authentication.
This check is the manifest’s only commitment to plaintext, and it MUST NOT be weakened or skipped. XChaCha20-Poly1305 is not key-committing: a ciphertext can pass authentication under more than one key, so the AEAD tag proves only that some key opened these bytes. The re-derived nonce proves that the decrypted bytes are the ones the creator sealed under this work’s key, because the nonce inside the signed ciphertext CID is a keyed function of exactly those bytes. Manifests publish no plaintext CIDs (§ 6.4.3 Chunk Encryption), so no other post-decryption check exists; this one carries the local-verification guarantee on the plaintext side, and the retrieval flow places it in sequence with the ciphertext check (§ 7.4 Retrieval).
6.4.5. Key Wrapping
The manifest’s wrapped_key is the work’s content key, sealed to the
creator’s declared public sealing key (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)) with HPKE, Hybrid Public Key
Encryption [RFC9180]: public-key encryption of a short value to a
recipient’s public key. The protocol uses a single HPKE suite for every
asymmetric encryption it performs, registered in
§ 11.6 Algorithm Identifiers and Parameters: mode base, DHKEM(X25519, HKDF-SHA256),
HKDF-SHA256, ChaCha20Poly1305. Both info and the associated data are
empty in every use. A sealed value is transmitted as the encapsulated key
followed by the ciphertext.
The same suite serves both wrapping directions. At publish, the client wraps
K_work to the declared sealing key, an X25519 public key (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)). At sale and
redemption, the sealer seals K_work to the grantee’s Ed25519 key,
converted to its X25519 form (§ 5.3.7 LicenseGrant (fondachi/license-grant), § 4.2 Keys and Signatures). The
conversion is the standard one between the two curves: the Edwards
public key maps to the Montgomery u-coordinate by the birational map of
[RFC7748], u = (1 + y) / (1 - y), and the X25519 private scalar is the
first 32 bytes of the SHA-512 digest of the Ed25519 seed, clamped as
[RFC7748] prescribes, which is exactly the scalar Ed25519 itself signs
with [RFC8032]. One primitive covers both directions, so no second
asymmetric algorithm enters the protocol.
wrapped_key is safe to publish, and every manifest does: only the sealing
private key opens it, so a mirror or crawler holding the manifest holds
nothing usable. Holders never read it; they receive keys sealed to
themselves, in the grant or by redemption (§ 6.6 Key Redemption). Under self-custody the
creator wraps to a sealing key they generated themselves, and the wrap works
identically.
6.5. Key Release and the Seal Boundary
A content key exists in the clear in exactly three places: on the publishing client that derived it, on a grantee’s client after unsealing it from their grant, and transiently inside a seal boundary while it is re-sealed from one form to the other. Everywhere else it is wrapped or sealed.
The seal boundary is the environment the sealer runs in (Appendix B. Glossary), and re-sealing happens entirely within it: the sale and redemption flows are where a content key crosses from the creator’s wrap to a grantee’s seal (§ 7.2 Purchase, § 8.5 Keys (Sealer Role)), and whatever the flow, the operator observes only the grantee-sealed output. Key material MUST NOT be released outside a seal boundary: a deployment in which the operator can read a content key in the clear does not provide the blindness of § 6.3 The Blind Host Model and is non-conformant, whatever it encrypts at rest.
Where the private sealing key is not held by the creator personally, it MUST be held so that it can be used but not extracted, in practice inside a key management system or hardware security module, and released only into the attested seal boundary, never to the operator. Such custody MUST keep one sealing key per creator; a multi-tenant host MUST NOT aggregate tenants under a shared key, which would make it a cross-tenant key concentrator, the exact shape this protocol exists to avoid. The release policy MUST be immutable once set, so the operator cannot later loosen it, and the creator verifies that policy once, directly against the key store, before signing the declaration that adopts the key (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)). The key store SHOULD log every unwrap and SHOULD rate-limit them. The key store guards the private sealing key and nothing else: chunk encryption never enters it (that happens on the publishing client), and the HPKE seal to the grantee runs inside the attested boundary.
What makes an environment a seal boundary is the property, not a mechanism. Self-custody by the creator, an attested trusted execution environment, and threshold or multi-party computation each satisfy it, with different residual trust, and § 10.7 The Active-Custodian Problem (Operator Forgery) analyzes those choices together with the settlement binding the sealer enforces. This section requires only that the property hold.
The minimum viable host is not yet characterized. Every conformant deployment needs a seal boundary, and the smallest arrangement that satisfies this section for a single-operator host remains to be described.
6.6. Key Redemption
Issuance delivers keys inline: every grant’s sealed_keys carry the content
key for each manifest current at issuance (§ 5.3.7 LicenseGrant (fondachi/license-grant)). Because a work’s content
key is fixed (§ 6.4.2 Per-Work Content Keys), those keys are also the last a holder ordinarily
needs: any sealed key for a work decrypts every revision of it, past and
future, so a new revision is fetched and read with the key already in hand
and no sealer is involved (§ 7.7 Revision Publish (Holder Side)). Redemption is how a
holder obtains a key sealed to their own key when issuance’s delivery does
not serve them, and it exists for exactly two cases:
-
Lost keys. Redemption is idempotent: a holder who lost a sealed key re-fetches it against their still-valid grant.
-
Rotated holders. A key rotation, an owner’s own or an heir’s takeover, leaves the grant in place and the sealed keys sealed to the signing key of the moment of sale, so the holder redeems keys sealed to the current key against the grant and the key-event chain (§ 5.3.7 LicenseGrant (fondachi/license-grant), § 7.6 Inheritance (Takeover)).
Redemption is served by the sealer; its wire surface, and the verifications the sealer performs before releasing a key, are specified in § 8.5 Keys (Sealer Role), under the settlement binding of § 10.7 The Active-Custodian Problem (Operator Forgery).
6.7. SealingKeyDeclaration (fondachi/sealing-key-declaration)
Publishing wraps each work’s content key to a public sealing key (§ 6.4.5 Key Wrapping); a creator declares that key by registering a SealingKeyDeclaration in their identity log:
sealing-key-declaration-body = { alg : "hpke-x25519-sha256-chacha20poly1305" , key : multikey , ; the public sealing key, X25519 creator : did , ; MUST name the signing identity issued_at : timestamp , unknown-fields , }
A SealingKeyDeclaration MUST be signed by the creator’s signing key
current at its issuance (§ 4.4 Key Events (Rotation and Recovery)), and creator MUST name the
creator’s identity. alg names the registered HPKE suite of § 6.4.5 Key Wrapping, its only
registered value; key is an X25519 public key, expressed as a Multikey
value exactly as verification keys are (§ 4.2 Keys and Signatures).
Discovery is the declaration’s job, and it is load-bearing at publish:
the creator’s current sealing key is the one named by the latest
SealingKeyDeclaration registered in their identity log, and the publish flow
resolves it from there, never from an operator’s word
(§ 7.1 Publish). A host may be the transport for that lookup,
serving the log as it serves any catalog data, but it is never the
authority; a client that wrapped to an operator-supplied key would hand
that operator every content key it publishes thereafter. Every manifest’s
wrapped_key (§ 6.4.5 Key Wrapping) targets the key so resolved, so publishing is
possible if and only if a declaration is registered, and the binding from
creator to sealing key is signed, replicated, and discoverable like any
other registered object.
Who generates the keypair depends on custody, and the declaration is what makes either arrangement creator-authorized. Under self-custody the creator generates the keypair and runs the sealer themselves, and their signature on the declaration is the whole trust. Under host-managed custody the operator provisions the keypair inside the key store or enclave of § 6.5 Key Release and the Seal Boundary, and the creator signs a declaration of the advertised public half after verifying, once, that the release policy is immutable and correctly pinned (§ 10.7 The Active-Custodian Problem (Operator Forgery)). In both cases what is declared is only ever the public half.
Rotation. A creator rotates by registering a new declaration; the latest registered declaration governs subsequent publishes, and wraps made under superseded declarations remain valid. Every sale and redemption of a manifest wrapped to an old key still requires that key to unwrap it, so sealers retain superseded keys (§ 8.5 Keys (Sealer Role)).
Loss. A lost private sealing key costs availability, never content: the
creator re-derives each affected work’s content key from content_master
(§ 6.4.2 Per-Work Content Keys) and publishes one new revision per work, carrying the same content
key wrapped to the current sealing key. The content key is unchanged and
encryption is deterministic (§ 6.4.3 Chunk Encryption), so the new revision references the
identical ciphertext chunks already stored, and the republish costs one
signed manifest per work.
Compromise. The converse does not hold: wrapped_key values are
published in manifests and the identity log is append-only, so a compromised
private sealing key exposes the content key of every manifest ever wrapped to
it, to any party that also obtains the ciphertext. Rotation protects
subsequent publishes only. The gate (§ 6.2 Gating) remains the bound on what that
exposure is worth, and § 10.2 Key Compromise and Rotation treats
sealing-key compromise alongside the other custody risks.
7. Protocol Flows
This section specifies the protocol in motion: how creators, holders, and the services between them (hosts, mirrors, and sealers) cooperate to publish, sell, and deliver content, and how ownership, catalogs, and identities survive what happens afterwards, from a host going out of service to a takedown. Numbered steps are normative where they constrain objects, messages, and the checks a party performs before acting; the objects exchanged in the steps are specified in § 5 Canonical Encoding and Object Model, and the endpoints they call in § 8 HTTP API. Every flow here is core except the subscription lifecycle (§ 7.5 Subscription Lifecycle), the one optional flow.
7.1. Publish
Publishing is client-side work. The creator’s client is the only machine that ever holds plaintext or a content key (§ 6.3 The Blind Host Model), and the host receives only ciphertext and signed objects.
-
The client ensures all objects to be published are registered in the creator’s identity log: the Work the content belongs to (§ 5.3.2 Work (fondachi/work)), and, before a creator’s first publish, a SealingKeyDeclaration (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)). Without a registered declaration there is nothing to wrap to, and publishing is impossible.
-
The client resolves the creator’s current sealing key: the latest SealingKeyDeclaration registered in the identity log, verified as signed by the creator’s identity key. It MUST NOT accept a sealing key on an operator’s word (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)).
-
The client derives the work’s content key (§ 6.4.2 Per-Work Content Keys), splits each file into chunks of the declared
chunk_size, encrypts every chunk (§ 6.4.3 Chunk Encryption), and computes the ciphertext chunk CIDs. It wraps the content key to the sealing key (§ 6.4.5 Key Wrapping), assembles the ContentManifest (§ 5.3.1 ContentManifest (fondachi/manifest)), and signs it with the creator’s signing key. A first publish carriesrev1; a revision is this same flow with the nextrev, re-storing only its changed chunks (§ 6.4.3 Chunk Encryption). -
The client uploads the ciphertext chunks and the signed objects to the host.
-
The host verifies that every stored chunk hashes to the
cipher_chunksCID the signed manifest declares, and MUST reject a chunk that does not (§ 6.3 The Blind Host Model). It MUST NOT accept plaintext content on any path. -
The host, under a live
publishdelegation (§ 5.3.6 Delegation (fondachi/delegation) and Revocation), appends the objects to the creator’s identity log, enforcing(work, rev)uniqueness at append (§ 5.3.1 ContentManifest (fondachi/manifest)), and advances the signed head (§ 5.3.11 Identity Log). From the head’s visibility the release is discoverable, its offers are saleable, and mirrors pick the new objects up by ordinary sync.
The material a storefront shows before purchase (covers, samples, listing pages) is not part of this flow: it travels out of band by host arrangement, or through the optional ReleaseAnnotations extension (§ 5.3.3 Release (fondachi/release), § 5.3.12 ReleaseAnnotations (fondachi/release-annotations), Optional Extension). The artwork and companion material a buyer owns needs no side channel, because it ships inside the payload as ordinary files (§ 5.3.1 ContentManifest (fondachi/manifest)).
Note: A blind host cannot inspect what it publishes, for new uploads and revisions alike, so acceptance policy can operate only on metadata and out-of-band material, and content moderation on this network flows through complaint and takedown (§ 7.10 Takedown Propagation, § 10.5 Content Encryption Considerations) rather than host inspection.
7.2. Purchase
A purchase runs through two parties with deliberately separate powers. The
host, holding a live sell delegation, executes the sale and signs the grant;
the sealer verifies the sale for itself and then releases the content keys.
The separation exists because a host can mint grants, so a grant alone is not
evidence of purchase; the settlement is the one input a host cannot forge
(§ 10.7 The Active-Custodian Problem (Operator Forgery)).
-
The buyer’s client fetches the Offer and validates it offline before any payment: the signature is the creator’s signing key (§ 5.3.5 Offer (fondachi/offer)), the offer is within its validity window, and the subject releases resolve in the creator’s catalog.
-
The client requests a payment intent from the selling host (§ 8.4 Purchase (Host Role)), naming the
granteeDID. The host MUST honor the presented grantee and MUST NOT require the buyer to hold an account or session; this guest checkout is what a brokered purchase builds on (§ 7.3 Cross-Host (Brokered) Purchase). -
The rail settles and the host’s rail adapter emits the settlement event (§ 8.6 Rails). The host verifies that the settlement satisfies the offer’s terms: the currency matches, and the amount is at least the price, or at least
pwyw_floorwhere the buyer names the amount. -
The host requests grantee-sealed keys from the sealer, presenting the offer, the settlement, the grantee, and the current manifest of each subject work. Inside the seal boundary, the sealer verifies the settlement binding: the settlement evidence is genuine for its rail, names the same subject and grantee, satisfies the creator-signed price, and pays the account the creator bound on that rail (§ 8.6 Rails). It MUST NOT release a key otherwise; § 10.7 The Active-Custodian Problem (Operator Forgery) explains why this check, and not the grant, is the sealer’s evidence of a sale. It then unwraps each manifest’s
wrapped_key, seals the content key to the grantee’s key, discards the plaintext keys, and returns only the sealed values (§ 6.5 Key Release and the Seal Boundary). -
The host assembles one LicenseGrant per subject release (§ 5.3.7 LicenseGrant (fondachi/license-grant)), signs each grant under its delegation, and returns the grants to the buyer. Where an offer names several releases, issuance MUST be atomic: every grant of the purchase is issued, or none is. The set shares its provenance, which is what lets one settlement reversal revoke exactly that set (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)).
-
The client MUST verify the grant chain offline before treating the purchase as complete: each grant is validly signed under a delegation live at issuance and chaining to the creator root, walking the root’s key events where its keys have rotated (§ 4.4.4 Verifying a Chain (Offline)); the embedded release is creator-signed and lists the granted works; and each
sealed_keysentry unseals with the holder’s own key.
An offer priced at zero waives settlement (§ 5.3.5 Offer (fondachi/offer)), and steps 2 through 3
fall away: the buyer requests issuance directly, the sealer verifies the
zero price against the creator-signed offer, and the resulting grant
carries no provenance.settlement (§ 5.3.7 LicenseGrant (fondachi/license-grant)).
7.3. Cross-Host (Brokered) Purchase
A buyer need not hold an account on the host that sells a work. Any host MAY execute flow § 7.2 Purchase on a buyer’s behalf, because guest checkout takes only a grantee DID; for a home host this is core: a host that surfaces federated content (§ 9 Federation) MUST let its users buy that content through it.
-
The buyer’s home host resolves the offer for the desired release from the selling host’s catalog, from its own synced copy or live (§ 9 Federation).
-
The home host runs flow § 7.2 Purchase against the selling host, presenting the holder’s DID as
grantee. The selling host collects payment and issues the grant; the protocol defines no cross-host settlement (§ 1.3 Scope of This Document), and how the home host charges its own user is host commerce, outside this document. -
The home host performs the verification of § 7.2 Purchase step 6 on the holder’s behalf, except the unseal check, which only the holder’s own key can perform, and files the grant in the holder’s library, recording the selling host as the content’s origin and storing the objects needed to verify and retrieve later: the delegation, the manifests, and the catalog’s mirror authorizations. The holder’s own client repeats the verification when it next syncs the library, since a home host is convenience, never a trust root (Appendix B. Glossary).
-
Retrieval is flow § 7.4 Retrieval, unchanged. The proof is minted by the holder’s client, because only the client holds the key (§ 4.3 Custody), audience-bound to the origin or one of its authorized mirrors. The home host MAY store the fetched ciphertext as that holder’s personal mirror (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)) and serve the holder itself from then on, which gives the library durability independent of the origin; this is an option a home host advertises, never an obligation, so a home host’s storage does not grow with its users' purchases on other instances unless it chooses to offer that.
The affordances this flow relies on are required of every host, including a single-creator storefront with no user accounts of its own: guest checkout (issue a grant to any presented grantee DID) and account-independent retrieval (serve any holder presenting a valid proof, § 7.4 Retrieval). A host with no users has no one to broker for, but other hosts broker from it, which is what makes every catalog in the federation buyable from anywhere (§ 9 Federation).
7.4. Retrieval
Every retrieval serves ciphertext through the gate, and there is no other path: content bytes MUST NOT be served outside this flow, never from open or public storage, and never to an unauthenticated requester. The client decrypts and verifies locally; the gate and the encryption defend against different adversaries, and this flow runs both (§ 6.2 Gating).
-
The client resolves candidate mirrors: the discovery document of the host serving the catalog lists mirror endpoints (§ 8.1 Discovery), and the client verifies a candidate by fetching its discovery document and checking that the service key it advertises holds a live MirrorAuthorization for the catalog (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)). A personal mirror holding bytes for this holder qualifies under the § 5.3.10 MirrorAuthorization (fondachi/mirror-auth) exception.
-
The client mints a RetrievalProof (§ 5.4 Retrieval Proofs) for the one chosen mirror, scoped to the manifests it needs. Only the holder’s client can mint one, because no other party holds the key (§ 4.3 Custody).
-
The client presents the proof with the grant chain (§ 8.3 Content (Mirror Role)). The mirror MUST verify before serving a byte: the chain is valid,
holderis the grant’s current holder (§ 5.3.7 LicenseGrant (fondachi/license-grant)), the audience is itself, the proof is unexpired, the subject is not taken down, and the grant is not named by arevocationentry in the catalog’s log (§ 5.4 Retrieval Proofs, § 5.3.11 Identity Log). The mirror MUST rate-limit the proofs it honors, keyed per grant (see below), and an over-limit refusal MUST be a typed error distinct from an authorization failure (§ 8.9 Errors). -
The client fetches the ciphertext chunks and MUST verify that each received byte string hashes to the manifest’s
cipher_chunksentry before decrypting it. -
The client obtains the content key: by unsealing a
sealed_keysentry from its grant with its own key (§ 5.3.7 LicenseGrant (fondachi/license-grant)), or by redemption where the signing key has rotated since the keys were sealed, or the key was lost (§ 6.6 Key Redemption). -
The client decrypts each chunk and runs both checks the construction requires, AEAD authentication (§ 6.4.3 Chunk Encryption) and the nonce commitment (§ 6.4.4 The Plaintext Commitment), rejecting any chunk that fails either.
The order is deliberate. The ciphertext check of step 4 runs before any key is touched, so a corrupt or malicious mirror is caught, and attributable, without decryption; the checks of step 6 then bind the decrypted bytes to the creator’s signature. Together they are what make an untrusted mirror safe to fetch from.
The per-grant rate limit of step 3 is what bounds a shared grant’s usefulness as bulk distribution: however many parties hold the holder’s signing key, the proofs they mint all name one grant, and each mirror honors them at a bounded rate. A shared proof buys less still, being bound to one mirror, scoped to named manifests, and expired within fifteen minutes (§ 5.4 Retrieval Proofs); replaying it inside that window is bounded by the service a mirror attaches to a single proof, which is the mirror’s own bandwidth policy. The limits are each operator’s choice, and mirrors do not coordinate them, so aggregate throughput scales with the number of authorized mirrors. No further quota is part of the protocol; anything else a mirror enforces is ordinary operator policy.
7.5. Subscription Lifecycle
Subscriptions are host-level commerce over the stream offers of § 5.3.5 Offer (fondachi/offer), and a conformant host need not offer them. The protocol surface is deliberately small: there is no subscription object, and nothing here creates a new kind of grant. The creator-signed stream offer is the authorization and the price anchor, and everything a subscriber accrues is an ordinary perpetual grant (§ 5.3.7 LicenseGrant (fondachi/license-grant)). A tier is a standing order: while the buyer keeps settling, what lands in the offer’s streams is bought.
-
The creator publishes one or more Streams, routes releases into them by registering StreamEntries (§ 5.3.4 Stream (fondachi/stream) and StreamEntry (fondachi/stream-entry)), and signs a stream offer whose subject is those streams (§ 5.3.5 Offer (fondachi/offer)).
-
The buyer subscribes at a host selling the offer, which establishes a recurring settlement (rail specifics are host-internal, § 8.6 Rails). Each period’s settlement is a distinct settlement reference.
-
For each StreamEntry registered in a subject stream while the buyer’s recurring settlement is active, the host issues a grant for the entry’s release, following § 7.2 Purchase steps 4 and 5, with
provenancenaming the offer and the settlement covering the entry’s registration period. The sealer’s coverage check for such an accrued grant is the creator-signed StreamEntry in a subject stream together with a settlement covering the entry’s registration period (§ 10.7 The Active-Custodian Problem (Operator Forgery)). -
Under
stream-complete, joining additionally issues grants for every release in the stream at that moment, funded by the first period’s settlement; the sealer’s check for a backlog grant is an entry registered before the first covered period together with thestream-completemodel on the creator-signed offer. Grants sharing one settlement issue atomically (§ 7.2 Purchase). Understream-future-only, joining conveys nothing already in the stream, and a back catalog is sold as an ordinary release (§ 5.3.5 Offer (fondachi/offer)). -
A reversal of one period’s settlement revokes exactly that period’s grants (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)); for
stream-complete, reversing the first period takes the backlog grants with it, since they share its settlement. -
Cancellation is the absence of the next settlement. Nothing is signed, nothing expires, and nothing is taken back: accrual stops, and every grant already issued is as permanent as any purchase.
7.6. Inheritance (Takeover)
A holder’s grants never change identities: they name a did:fnd, and
the DID is for life (§ 4.1 Identifiers). What changes hands is the identity
itself, by key event (§ 4.4 Key Events (Rotation and Recovery)): inheritance is an heir, holding a
rotation key they were given, rotating the identity’s keys to their own.
The library, the DID, and every grant come with it. There is no mechanism
for consolidating one identity’s grants into another; an heir who wants
one library operates one identity per bequest.
-
The heir signs a key event replacing the
signing_keywith a key of their own (§ 4.4.3 Later Events). Nothing here differs from the owner’s own rotation, and the owner need not participate: the rotation key the heir was given is the authorization. -
The event is registered in the identity’s log by its registrar (§ 4.4.5 Registration, Conflicts, and the Recovery Window); an unregistered event verifies offline but loses conflicts, so an heir SHOULD insist on registration, and should expect the recovery window, within which a stronger key can displace the takeover, to pass before relying on it. An identity whose log no host serves has no registrar until one again does (§ 7.9 Holder Host Migration).
-
From registration, the heir’s key is the identity’s signing key. A mirror that has seen the event, by sync or by presentation, MUST refuse proofs minted by the superseded key (§ 5.4 Retrieval Proofs); a mirror that has not seen it cannot know the key has rotated, which is the conflict rule of § 4.4.5 Registration, Conflicts, and the Recovery Window in effect.
-
The heir obtains content keys by redemption against the grant and the key-event chain (§ 6.6 Key Redemption, § 8.5 Keys (Sealer Role)):
sealed_keysare sealed to the signing key of the moment of sale and never move, and the sealer’s rate limits stay in the loop. -
Revocation follows the settlement, not the holder (§ 5.3.8 GrantRevocation (fondachi/grant-revocation)). Clients SHOULD surface a settlement’s age to an heir, so nobody is surprised by a reversible purchase.
7.7. Revision Publish (Holder Side)
On the creator’s side a revision is just flow § 7.1 Publish with the next rev. This
flow is the holder’s side: how an existing holder receives it.
-
The holder’s client learns of the new revision from the creator’s log head, by polling or by its home host’s notification (§ 8.2 Catalog (Host Role)).
-
Entitlement is a single offline check: the new manifest’s signed
workis among the grant’s subject works (§ 5.3.7 LicenseGrant (fondachi/license-grant)). There is no re-purchase and no policy to evaluate; every grant follows every revision of its works. -
The client fetches the revision per flow § 7.4 Retrieval, with the proof scoped to the new manifest.
-
No new key is needed and no sealer is involved: the sealed keys the holder already has decrypt every revision (§ 6.6 Key Redemption). A new revision is therefore readable by every existing holder the moment the log head advances, even while the sealer is unreachable.
7.8. Creator Host Migration
A creator moves between hosts by signatures and sync. No key or secret
changes hands in this flow: the signing key and content_master never
leave the creator’s client (§ 4.3 Custody, § 6.4.1 The Content Master Secret), and a sealing
private key is never moved between operators, only superseded (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)).
-
The creator signs a MirrorAuthorization for the new host’s service key (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)). The new host syncs the registered objects and the ciphertext (§ 8.3 Content (Mirror Role), § 9 Federation), verifying every chunk against the manifests as any mirror does, and serves as an ordinary mirror during the transition.
-
The creator signs a Delegation to the new host and a DelegationRevocation for the old one; the revocation’s
effective_atMAY be set in the future so both delegations are live across a wind-down window (§ 5.3.6 Delegation (fondachi/delegation) and Revocation). Existing grants remain valid forever under the at-issuance rule (§ 4.2 Keys and Signatures); new sales chain to the new delegation. -
The creator registers a LogContinuation naming the log’s head at the old host (§ 5.3.11 Identity Log). The new host already holds the full log from step 1’s sync; it verifies the imported chain up to that head, appends the continuation, and signs heads from there. The same identity log continues on the new host, every entry intact; nothing is re-signed and nothing restarts (§ 5.3.11 Identity Log).
-
Seal custody moves by supersession. Under self-custody nothing changes. Where the old operator managed the sealing key (§ 6.5 Key Release and the Seal Boundary), the creator registers a new SealingKeyDeclaration under the new arrangement, which governs subsequent publishes (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)). Manifests wrapped to the old key still need the old seal boundary at sale and redemption; where that boundary will not remain available, the creator republishes one re-wrapped revision per affected work (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)), after which the old operator is not needed at all.
Nothing in this flow depends on the old host’s cooperation beyond staying reachable while the sync completes: a host that vanishes without notice costs availability and the re-wrap republish of step 4, never ownership.
Note: Announcing a shutdown well in advance is ordinary good operation, not
a protocol matter: no protocol behavior conditions on the announcement, and
a host that stays reachable through its wind-down lets creators migrate and
holders sync without loss. The standard signal is the Sunset header
[RFC8594] on the service’s responses, carrying the planned shutdown time,
optionally with a Link of relation sunset pointing at details.
7.9. Holder Host Migration
A holder’s standing never depended on their host: the DID is their own (§ 4.1 Identifiers), grants verify offline (§ 5.3.7 LicenseGrant (fondachi/license-grant)), and bytes are re-fetchable from any authorized mirror (§ 7.4 Retrieval). What a home host provides is brokering (§ 7.3 Cross-Host (Brokered) Purchase), the library, and registrar duty for the holder’s log (§ 5.3.11 Identity Log); migration moves exactly those.
-
The holder establishes the relationship with the new home host and lodges their library with it: the grant envelopes and the supporting objects. The client supplies them from its own copies, or re-fetches them from the old host’s grant surface while it lives, authenticated as the grantee (§ 8.4 Purchase (Host Role)).
-
The new home host imports the holder’s log whole: every entry, fetched from the old host while it lives or supplied from the client’s retained copies and signed head checkpoints. The holder signs a LogContinuation naming the log’s head at the old host (§ 5.3.11 Identity Log); the new host verifies the imported chain up to that head, appends the continuation, and takes over registrar duty, signing heads from there. The same log continues, never restarts: the first-declaration-wins anchor of § 4.4.2 Rotation Keys and Pre-Rotation depends on its history staying whole.
-
Content needs no migration of its own. The client re-fetches per flow § 7.4 Retrieval as needed, and the new home host MAY store the ciphertext as the holder’s personal mirror (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)), on the same optional terms as in § 7.3 Cross-Host (Brokered) Purchase.
-
A holder MAY end at no home host at all. Everything that constitutes ownership survives: grants, keys, and retrieval against any authorized mirror. What lapses is the registrar relationship, so key events signed in the interim are registerable only once a host again serves the holder’s log, and until then they verify offline but lose conflicts (§ 4.4.5 Registration, Conflicts, and the Recovery Window).
7.10. Takedown Propagation
This flow propagates the two actions of § 5.3.9 Takedown / Delist (fondachi/takedown), delist and
takedown: how they move through the network, and when they bind each
operator.
-
The creator root, or a key holding a live
takedowndelegation, signs the Takedown and registers it in the creator’s identity log; the host SHOULD additionally push it to the mirrors it knows (§ 8.8 Server-to-Server). The liveness requirement is what keeps a host whose delegation has lapsed from reaching into a catalog it no longer serves (§ 5.3.9 Takedown / Delist (fondachi/takedown)). -
From visibility, hosts MUST refuse new sales of the subject. For a
takedown, every mirror MUST stop serving the subject’s bytes once the record is visible to it; a mirror that missed the push sees it by the head polling its sync already performs (§ 9.3 The Crawl (Transitive / Gossip)), which bounds how long propagation takes. -
A subject’s effective state is computed in log order, under the per-action validity rules of § 5.3.9 Takedown / Delist (fondachi/takedown): registration order in the catalog log settles what is in force, and no out-of-band ordering exists.
-
Existing holders are outside both actions' reach: neither action deletes a grant, and a holder’s local copies are beyond the protocol entirely (§ 5.3.9 Takedown / Delist (fondachi/takedown)).
-
Where neither the creator nor a live
takedowndelegation issues one, no network-wide obligation exists in the creator’s name, and there is nothing to propagate. Each operator then acts unilaterally on its own infrastructure as legal obligations reach it, which requires no object and no authority from anyone (§ 5.3.9 Takedown / Delist (fondachi/takedown)). The cooperative path and the unilateral one differ only in reach: the delegated object binds the network, while an operator’s own compliance binds itself.
8. HTTP API
This section specifies the wire surface of hosts, mirrors, and sealers: the endpoints called by the flows of § 7 Protocol Flows. Behavioral requirements (what a party verifies before acting) live with the flows; this section owns the surface itself.
All endpoints live under the /fondachi/ path prefix, except the discovery
document at its well-known URI (§ 11.1 Well-Known URIs). All
transport is HTTPS.
Representations. A signed object travels as its canonical DAG-CBOR bytes
(§ 5.1 Canonical Encoding), under the media type
application/vnd.ipld.dag-cbor, in requests and responses alike. Endpoints
that serve or accept signed objects MUST support this representation; it is
the authoritative one, and receiving it makes verification direct, since the
bytes on the wire are the bytes that were signed and hashed. A server MAY
additionally offer DAG-JSON views (application/vnd.ipld.dag-json),
selected by the Accept header: DAG-JSON [DAG-JSON] is the bijective
JSON codec for the same data model, readable by humans and tooling. A view is
non-authoritative: a verifier MUST NOT trust a JSON view it has not
re-encoded to canonical DAG-CBOR and checked. Composite responses (logs,
listings) are DAG-JSON objects that embed envelopes; purely operational
payloads that carry nothing signed (the discovery document, payment
intents, sessions, errors) are plain JSON. Content chunks and blobs are
application/octet-stream.
Authentication. Four kinds of credential appear on this surface, none of them a host account:
-
A RetrievalProof (§ 5.4 Retrieval Proofs) authorizes retrieval, presented to the mirror it names (§ 8.3 Content (Mirror Role)).
-
A holder credential authenticates a holder to read their own records (§ 8.4 Purchase (Host Role)). It is a RetrievalProof with an empty
scope, audience-bound to the server and expiring like any proof; the empty scope means it authorizes no content retrieval. -
A session token returned by § 8.3 Content (Mirror Role)’s retrieval endpoints is a bearer credential for the blob fetches of one authorized session.
-
Server-to-server requests are authenticated by an HTTP signature from the sending service’s key, verifiable against its discovery document (§ 8.8 Server-to-Server).
Where an endpoint instead says it is host-authenticated (publishing uploads, purchase checkout), the mechanism is the host’s own account or session machinery, outside this protocol; the protocol constrains what the endpoint accepts, never how its operator identifies customers.
8.1. Discovery
GET /.well-known/fondachi returns the discovery document, plain JSON:
{ "spec" : "0.1" , "service_key" : "did:key:z6Mk..." , // this service's signing identity "creators" : [ { "did" : "did:fnd:bafyrei..." , "handle" : "myband" , // display hint "domains" : [ "example.band" ], // domain-anchor hints (identity chapter) "sealer" : "https://seal.example" , // base URL of this creator's sealer (sealer role) "mirrors" : [ "https://m1.example" ] // endpoint hints for this catalog's mirrors } ], "peers" : [ "https://other.example" ], // federation peers (federation chapter) "extensions" : [ "release-annotations" , "oidc" ], "oidc_issuer" : "https://host.example" // present iff "oidc" is listed }
The discovery document is operational fact, not signed catalog data, and a
reader treats it accordingly: its claims are hints until cross-checked
against signed objects. A creator entry’s mirrors list locates candidate
mirrors for that creator’s catalog, and only locates them: who is
authorized to mirror is said by the live MirrorAuthorizations in the
creator’s own identity log (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)), and a candidate qualifies when its own
discovery document advertises a service_key holding one
(§ 7.4 Retrieval). The hints are per creator because mirroring is
authorized creator by creator; a hint from any source works equally, and a
stale or missing hint costs discoverability, never authority. A domains
entry is believed when the domain’s anchor file lists the DID
(§ 4.5.3 Domain Anchor); a sealer entry merely locates the sealer,
whose keys are trusted only through the creator-signed
SealingKeyDeclaration (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)). The document is
also how a DID is confirmed to control an endpoint: the endpoint advertises
the service_key, and whoever answers there holds it.
extensions announces the optional extensions this service implements, by
registered name (§ 11.4 Extension Names), so clients and indexers
can probe capabilities without trial requests.
8.2. Catalog (Host Role)
The catalog surface serves an identity’s log (§ 5.3.11 Identity Log) and accepts a creator’s signed uploads.
-
GET /fondachi/catalog/{identity}/headreturns the current LogHead (§ 5.3.11 Identity Log), an envelope signed by the serving host’sservice_key. Followers poll this; the signature is what makes equivocation attributable. -
GET /fondachi/catalog/{identity}/log?since={cid}&limit={n}returns{ "entries": [ { "entry": LogEntry, "object": Envelope | null } ], "next": CID | null }: the log entries in order, resuming after the entrysince, each with its registered object inline where the entry is of kindobjectandnullwhere it is of kindrevocation. -
GET /fondachi/object/{cid}returns one published object as its canonical bytes, so the response body hashes to the requested CID. Published objects only: a host MUST NOT serve grant or revocation bodies on any public endpoint. A revocation is visible only as its log entry, which names the revoked grant by CID; the bodies are available to the parties § 5.3.11 Identity Log names, through the authenticated endpoints of § 8.4 Purchase (Host Role).
Publishing uploads are host-authenticated (see the preamble):
-
POST /fondachi/publish/objectaccepts one pre-signed envelope as canonical bytes, verifies it (signature, and the append-time rules of § 7.1 Publish), appends it to the signer’s identity log, and returns{ "cid": CID }. -
PUT /fondachi/blob/{cid}accepts ciphertext chunk bytes. The host MUST verify the body hashes to{cid}and MUST refuse the upload otherwise; acceptance of a chunk no manifest references is host policy.
8.3. Content (Mirror Role)
The content surface serves ciphertext under the gate, and nothing else; the retrieval flow is the only serving path (§ 7.4 Retrieval).
-
POST /fondachi/retrievewith body{ "proof": RetrievalProof, "chain": [Envelope, ...] }returns{ "session": token, "expires_at": t }after the verifications of § 7.4 Retrieval.chaincarries the grant, the grantee’s key events where its keys have rotated, and optionally the delegation inline, so any mirror, including one that never saw the sale, verifies the whole chain against the creator root from its synced catalog; a delegation is resolved by the CID in the grant’s signature entry, inline first, synced catalog second. The issuing host MAY accept a bare proof for grants it already holds. -
GET /fondachi/blob/{cid}, underAuthorization: Bearer <session>, returns the chunk bytes, or a temporary redirect to a delegated URL: a short-lived, non-enumerable URL at which the mirror’s storage serves the bytes directly, so large transfers need not pass through the mirror itself. A delegated URL is the gated path by another route, not a bypass of it: it MUST be minted only under a session this endpoint would have served, MUST expire, and the storage behind it MUST NOT be otherwise publicly readable (§ 6.2 Gating). Content addressing is what makes the delegation safe, since the client verifies every received chunk against the manifest no matter what serves it (§ 7.4 Retrieval). Range requests SHOULD be supported by whichever origin serves the bytes; a partial chunk cannot be authenticated or decrypted before it is complete (§ 6.4 Content Keys), so ranges buy resumption of an interrupted transfer, nothing more. -
POST /fondachi/mirror/sessionopens a bulk-fetch session for an authorized mirror: the body is a RetrievalProof with emptyscopeandgrant_chain, signed by the mirror’s service key, and the server checks a live MirrorAuthorization for that key (§ 5.3.10 MirrorAuthorization (fondachi/mirror-auth)). The session covers every chunk of every published, non-taken-down manifest, with a TTL of at most one hour. The request MAY include anendpoint, the caller’s public base URL; the server records it as a push target for § 8.8 Server-to-Server.
There is no plaintext endpoint, no rendition or playlist endpoint, and no unauthenticated chunk access. A client that wants to stream plays the files its payload provides, fetching chunks through the session like any other retrieval.
8.4. Purchase (Host Role)
-
POST /fondachi/purchase/intentwith body{ "offer": CID, "grantee": DID, "amount": n? }starts a purchase (amountis required above a pay-what-you-want floor). The response is a rail descriptor,{ "rail": "stripe", ... }, whose contents are rail-specific and opaque to this protocol; an instant rail MAY settle inline and return the signed grants directly. For a zero-price offer there is no rail descriptor: issuance proceeds immediately (§ 7.2 Purchase). The endpoint accepts any presentedgranteeDID; the purchase flow prohibits conditioning it on an account, which is what makes brokered purchase universal (§ 7.2 Purchase, § 7.3 Cross-Host (Brokered) Purchase). -
GET /fondachi/grants, authenticated with a holder credential, returns{ "grants": [Envelope, ...] }: the grant objects held by this host that name the caller’s identity (§ 5.3.7 LicenseGrant (fondachi/license-grant)).
8.5. Keys (Sealer Role)
The endpoints of this section are served by the sealer, never by the host
as such, even where one operator provides both
(§ 6.5 Key Release and the Seal Boundary). A sealer is always reachable as a
service, because sealing is driven by buyer events that occur whether or
not the creator is online; under self-custody that means the creator runs,
or delegates the running of, an endpoint, not that they are present. A
creator’s sealer is located through the sealer entry of a discovery
document (§ 8.1 Discovery).
-
POST /fondachi/keysredeems keys for a holder. The body is{ "proof": RetrievalProof, "manifests": [CID, ...] }; the response is{ "sealed_keys": [ { "manifest": CID, "alg": ..., "ct": bytes } ] }, each entry the content key sealed to the holder’s key (§ 6.4.5 Key Wrapping). Before sealing anything, the sealer MUST verify the grant chain and the holder’s signature exactly as a mirror does before serving bytes, and MUST release a key only where the chain’s provenance satisfies the settlement binding of § 7.2 Purchase step 4, or names a creator-signed zero-price offer (§ 5.3.5 Offer (fondachi/offer)). The endpoint MUST be idempotent, so a holder who lost a key re-fetches it against their still-valid grant, and SHOULD be rate-limited per grant. Redemption exists for lost keys and succeeded holders (§ 6.6 Key Redemption). -
POST /fondachi/sealseals keys at sale, called by the selling host during the purchase flow (§ 7.2 Purchase). The body is{ "offer": CID, "settlement": <rail evidence>, "grantee": DID, "manifests": [CID, ...] }; the response issealed_keysas above, sealed tograntee. The sealer MUST verify, inside the seal boundary, that the settlement evidence is genuine for its rail, satisfies the creator-signed offer, names the same subject and grantee, and pays the account bound by the creator’s CreatorRail: the settlement binding of § 7.2 Purchase step 4 (§ 8.6 Rails); a zero-price offer waives the settlement and the field is absent. For subscription accrual the same endpoint serves each entry’s issuance, and the evidence covers the entry’s registration period (§ 7.5 Subscription Lifecycle).
A sealer SHOULD retain superseded private sealing keys: every sale and every redemption of a manifest wrapped under an old declaration still requires that key to unwrap it (§ 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)).
8.6. Rails
Rail adapters are host-internal; the protocol defines only what crosses
its boundary. The public surface is
POST /fondachi/rail/{rail}/webhook, authenticated per the rail’s own
convention (for example, a signature header), whose settlement and
reversal events feed grant issuance and revocation
(§ 5.3.8 GrantRevocation (fondachi/grant-revocation), § 7.2 Purchase). No other rail
state is exposed.
Settlement evidence formats are rail-specific and unspecified beyond the CreatorRail account-match requirement; per-rail profiles may deserve an appendix.
What makes a settlement attributable to a creator is the CreatorRail object:
creator-rail-body = { rail : tstr, ; settlement rail, e.g. "stripe" account : tstr, ; rail account id the creator is paid at creator : did , ; MUST equal the signer issued_at : timestamp , unknown-fields , }
A CreatorRail MUST be signed by the creator’s signing key current at its
issuance (§ 4.4 Key Events (Rotation and Recovery)), with creator naming the creator’s identity,
and is registered in the creator’s identity log. It binds the
identity to its payout account on a named rail, so a sealer verifying
settlement evidence resolves the CreatorRail matching the settlement’s
rail, confirms the binding chains to the grant’s creator root, and
requires the payment’s destination to equal account. An operator cannot
redirect the "paid the creator" check at an account it controls, because
the binding is creator-signed
(§ 10.7 The Active-Custodian Problem (Operator Forgery)).
8.7. Optional Extension: OIDC Ownership Assertion (Host as IdP)
This extension is optional: a conformant host need not implement it, and clients and mirrors never depend on it. It exists so a third-party web application can check what a holder owns through a standard OpenID Connect flow [OIDC]: an assertion of ownership, never a retrieval capability. Standardizing it makes "sign in with Fondachi" uniform across hosts.
A host offering it lists oidc in its discovery extensions and serves a
standard OIDC issuer (discovery at /.well-known/openid-configuration),
extended with:
-
scope
fondachi_grants, with request parameterfondachi_subjects=[CID, ...], yielding the ID-token claimfondachi_grants: the list of requested work CIDs for which the authenticated holder consents to disclose a valid grant. -
Tokens are audience-bound assertions about ownership. A mirror MUST NOT accept them for retrieval; the only retrieval credential is a RetrievalProof signed by the holder’s key (§ 7.4 Retrieval).
The canonical consumer is an owner-gated companion page: a creator’s web page that admits holders of their work, verified by this flow against the holder’s home host. One known limit follows from the architecture: a holder with no home host has no issuer, and cannot log in to such a page; their ownership is no less real, only unasserted (Appendix B. Glossary).
8.8. Server-to-Server
-
POST /fondachi/notifywith body{ "takedowns": [Envelope, ...], "heads": [ { "identity": DID, "head": CID } ] }pushes takedown records and fresh log heads to a peer or registered mirror, authenticated by an HTTP signature from the sending service’s key. The receiver MUST verify each takedown’s chain against a catalog it carries before acting on it (§ 7.10 Takedown Propagation). Delivery is advisory and need not be reliable: head polling is the backstop, and visibility obligations run on the pull path (§ 7.10 Takedown Propagation, § 9 Federation).
There is no server-to-server migration surface. Host migration is creator-driven and needs only ordinary mirror sync (§ 7.8 Creator Host Migration); no custody of any secret ever moves between operators.
8.9. Errors
Errors are returned as plain JSON, { "error": "<code>", "detail": ... },
where code is an identifier from the error-code registry
(§ 11.7 Error Codes) and detail is optional human-readable
context, carrying no protocol meaning. A server MUST use the registered
code where the registry names its condition, so that clients and the test
vectors can match on it.
HTTP status classes follow the failure’s nature: malformed or invalid
objects and unsatisfied validity rules are 400; missing or failed
credentials are 401; a valid credential refused by policy (a takedown,
a registration refusal) is 403; an unknown CID or identity is 404; a
registration conflict is 409; an over-limit refusal is 429, and MUST
be distinguishable from an authorization failure (§ 7.4 Retrieval).
9. Federation
9.1. Why It Composes
9.2. Discovery and Peering
9.3. The Crawl (Transitive / Gossip)
9.4. Authority, Sales, and Delivery
9.5. Trust
10. Security Considerations
10.1. Threat Model
10.2. Key Compromise and Rotation
10.3. Retrieval Proof Abuse
10.4. Grant Revocation and Settlement Reversal
10.5. Content Encryption Considerations
10.6. Federation and Trust Boundaries
10.7. The Active-Custodian Problem (Operator Forgery)
10.8. Conveyance by Takeover
11. Registrations and Well-Known Resources
This protocol has no live registry authority; this section is the one authoritative enumeration of the identifiers the other sections define. Pre-1.0, an identifier is added, changed, or retired only by a revision of this document, and the test vectors of Appendix A. Test Vectors pin the registered values in use.
11.1. Well-Known URIs
Two resources under the well-known prefix of [RFC8615]:
| Suffix | Resource | Specified in |
|---|---|---|
fondachi
| The discovery document, JSON | § 8.1 Discovery |
fondachi-creators
| The domain anchor file: the creator
did:keys a domain vouches for, in plain text, one per
line
| § 4.5.3 Domain Anchor |
A host offering the OIDC extension additionally serves the standard
openid-configuration well-known resource, which is OIDC’s registration,
not this document’s (§ 8.7 Optional Extension: OIDC Ownership Assertion (Host as IdP)).
11.2. Object Type Identifiers
The fondachi/<type> tags carried by signature envelopes (§ 5.2 Signature Envelope). Every
type’s body is specified in the section named.
fondachi/profile is reserved for the profile document, whose body this
revision does not specify. The RetrievalProof (§ 5.4 Retrieval Proofs) is deliberately absent:
it is a short-lived credential, not an envelope, and never enters a log.
The log entry itself (§ 5.3.11 Identity Log) is likewise not an envelope and
is not registered; the LogHead is an envelope, registered above, that a
host signs about a log without the head ever entering one.
An implementation that encounters an envelope of a type it does not recognize in a identity log MUST preserve and replicate it unaltered, and takes no other action on it. This is not tolerance for its own sake: log entries are hash-linked, so an implementation that dropped unknown entries would break every chain that contains one.
11.3. Names and Prefixes
The protocol’s reserved names, so that nothing else squats on them and extensions know where to put their own:
| Prefix or name | Use | Specified in |
|---|---|---|
fondachi/
| Envelope type tags; HKDF derivation labels | § 11.2 Object Type Identifiers, § 11.6 Algorithm Identifiers and Parameters |
/fondachi/
| The HTTP path prefix of every protocol endpoint | § 8 HTTP API |
fondachi, fondachi-creators
| Well-known URI suffixes | § 11.1 Well-Known URIs |
fondachi_grants, fondachi_subjects
| The OIDC extension’s scope, claim, and request parameter | § 8.7 Optional Extension: OIDC Ownership Assertion (Host as IdP) |
fondachi-sl-1
| The Standard License Terms version identifier grants bind to | § 5.3.7 LicenseGrant (fondachi/license-grant) |
The Standard License Terms are published separately; this document registers only the identifier (§ 1.3 Scope of This Document).
11.4. Extension Names
The names by which a discovery document’s extensions list announces the
optional extensions a service implements (§ 8.1 Discovery):
| Name | Extension |
|---|---|
release-annotations
| ReleaseAnnotations and its public asset surface (§ 5.3.12 ReleaseAnnotations (fondachi/release-annotations), Optional Extension) |
oidc
| The OIDC ownership assertion (§ 8.7 Optional Extension: OIDC Ownership Assertion (Host as IdP)) |
11.5. Media Types
The media types the HTTP API uses (§ 8 HTTP API). None is defined by this document; all are adopted:
| Media type | Used for |
|---|---|
application/vnd.ipld.dag-cbor
| Signed objects as canonical bytes, requests and responses |
application/vnd.ipld.dag-json
| Non-authoritative views and composite responses |
application/json
| Operational payloads carrying nothing signed |
application/octet-stream
| Content chunks and blobs |
11.6. Algorithm Identifiers and Parameters
The registered values of every algorithm-bearing field, and the fixed parameters of the constructions. The constructions themselves are specified in the sections named.
Signatures. alg: "ed25519" is the only registered envelope signature
algorithm (§ 5.2 Signature Envelope): Ed25519 [RFC8032].
Content encryption. enc.alg: "xchacha20poly1305" is the only
registered content AEAD (§ 5.3.1 ContentManifest (fondachi/manifest)): XChaCha20-Poly1305 [XCHACHA], 24-byte
nonce, empty associated data, used with the synthetic nonce of § 6.4.3 Chunk Encryption.
HPKE. "hpke-x25519-sha256-chacha20poly1305" is the only registered
HPKE suite, used for both wrapping directions (§ 5.3.7 LicenseGrant (fondachi/license-grant), § 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)): HPKE [RFC9180]
mode base, KEM DHKEM(X25519, HKDF-SHA256), KDF HKDF-SHA256, AEAD
ChaCha20Poly1305, with empty info and associated data. A sealed value
travels as the encapsulated key followed by the ciphertext; grantee keys
are converted from Ed25519 by the map of [RFC7748] (§ 6.4.5 Key Wrapping).
Key derivation labels. Every derivation is HKDF-SHA256 [RFC5869] with an empty salt and a label-first info string (§ 6.4 Content Keys). The registered labels:
| Label | Derives | Specified in |
|---|---|---|
fondachi/content-master/v1
| content_master from a backup
seed
| § 6.4.1 The Content Master Secret |
fondachi/content-key/v1
| A work’s content key | § 6.4.2 Per-Work Content Keys |
fondachi/content-nonce/v1
| A chunk’s synthetic nonce | § 6.4.3 Chunk Encryption |
Identifiers. Object references are CIDv1 [CID] with the sha2-256
multihash (code 0x12), codec dag-cbor (0x71) for objects and raw
(0x55) for chunks and blobs (§ 5.1 Canonical Encoding). Multibase [MULTIBASE] encoding is
base58btc, prefix z, wherever keys are rendered as text (§ 4.2 Keys and Signatures). The
registered multicodec [MULTICODEC] key prefixes are ed25519-pub (0xed)
for identity and service keys, and x25519-pub (0xec) for sealing keys
(§ 4.2 Keys and Signatures, § 6.7 SealingKeyDeclaration (fondachi/sealing-key-declaration)).
11.7. Error Codes
The registered error codes: stable strings, part of the spec surface. They are the vocabulary of the vectors' expected-outcome fields (Appendix A. Test Vectors) and of the HTTP error responses that § 8.9 Errors maps onto status codes. A producer MUST use the registered code where this table names its condition.
| Code | Condition | Specified in |
|---|---|---|
encode
| Not canonical DAG-CBOR, or undecodable | § 5.1 Canonical Encoding |
bad-signature
| Envelope signature verification failed | § 5.2 Signature Envelope |
bad-did
| Malformed identifier, or a DID method other than
the one its position requires (did:fnd for identities,
did:key for keys)
| § 4.1 Identifiers |
unsupported-alg
| An algorithm identifier not registered in § 11.6 Algorithm Identifiers and Parameters | § 5.2 Signature Envelope |
delegation-invalid
| Delegation missing, out of validity, or revoked at issuance | § 5.3.6 Delegation (fondachi/delegation) and Revocation |
broken-chain
| A grant chain that fails verification | § 5.3.7 LicenseGrant (fondachi/license-grant), § 7.2 Purchase |
wrong-holder
| Signer is not the holder’s current signing key | § 5.4 Retrieval Proofs, § 7.6 Inheritance (Takeover) |
not-rotation-key
| A key event signed by a key its predecessor does not authorize | § 4.4.3 Later Events |
key-event-chain-too-long
| A key-event walk past 256 events | § 4.4.4 Verifying a Chain (Offline) |
conflict
| A registration conflicting with an
already-registered entry, such as a second manifest at an occupied
(work, rev)
| § 7.1 Publish |
revoked
| A grant revoked by settlement reversal | § 5.3.8 GrantRevocation (fondachi/grant-revocation) |
invalid-revocation
| A revocation whose cause is not a settlement reversal | § 5.3.8 GrantRevocation (fondachi/grant-revocation) |
proof-expired
| A retrieval proof past its expiry | § 5.4 Retrieval Proofs |
wrong-audience
| A proof whose audience is not this server | § 5.4 Retrieval Proofs |
taken-down
| A subject the mirror must not serve | § 7.4 Retrieval, § 7.10 Takedown Propagation |
over-quota
| A per-grant rate-limit refusal, distinct from authorization failure | § 7.4 Retrieval |
not-held
| An object, blob, or identity this server does not hold | § 8 HTTP API |
settlement-unverified
| The sealer’s refusal of the settlement binding: evidence not genuine, price unmet, subject or grantee mismatched, or the payout account unbound | § 8.5 Keys (Sealer Role) |
Appendix A. Test Vectors
A.1 Suite Index
The vector suites are pending a re-cut against this revision’s object model; this index is filled when they are regenerated.
Error-mapping vectors (endpoint to expected registered code, § 11.7 Error Codes) would extend the suites.
A.2 Fixture Format
A.3 Regenerating the Vectors
Appendix B. Glossary
This appendix collects the terms this document uses. Entries are definitional; the rules that govern a term live only in the owning sections, and where an entry appears to conflict with them, the sections govern.
- Catalog
-
The body of work a creator publishes: the works, manifests, releases, streams, offers, and annotations registered in the creator’s identity log and served publicly by its host (§ 5.3.11 Identity Log, § 8.2 Catalog (Host Role)). An identity that publishes nothing has no catalog; its log carries only its key events.
- Client
-
Software acting on behalf of a holder or a creator. A client publishes, purchases, retrieves, and verifies; it serves no protocol surface of its own (§ 4.3 Custody, § 7 Protocol Flows).
- Content key
-
The symmetric key a work’s content bytes are encrypted under. Each work has its own content key, derived from a secret the creator holds; it is sealed to each grantee at purchase and unwrapped only within a seal boundary. Content keys encrypt and never sign; they are distinct from the signing keys of § 4.2 Keys and Signatures (§ 6.4 Content Keys).
- Creator
-
A DID that publishes works and issues licenses (see Holder).
- Delegation (
fondachi/delegation) -
A signed, narrow authorization from a creator to another DID, typically a host, permitting it to act on the creator’s behalf within a stated scope (§ 5.3.6 Delegation (fondachi/delegation) and Revocation).
- DID
-
A decentralized identifier as defined in [DID-CORE]. Every identity in this protocol is a DID (§ 4.1 Identifiers).
- Grant (
fondachi/license-grant) -
A signed LicenseGrant object, and the unit of ownership. A grant embeds the creator-signed release it was sold under, so a holder can see what they bought without consulting any server (§ 5.3.7 LicenseGrant (fondachi/license-grant)).
- Grant chain
-
The sequence of signed objects by which a grant is verified: the grant, the delegation under which it was issued, the creator root, and the key events of any identity in it whose keys have rotated (§ 5.3.7 LicenseGrant (fondachi/license-grant)).
- Holder
-
A DID that holds one or more grants. Holder and creator are roles a DID plays, not disjoint account types; a single DID can both hold grants and issue them. This document says holder for the protocol role, and reserves user for a person’s account relationship with a host, which is outside this document’s scope: the protocol sees only DIDs and signatures, and a DID that holds no grant has no standing here.
- Home host
-
The host an identity has asked to act on its behalf: it serves the identity’s log and acts as its registrar, and it is the identity’s agent in the flows, selling and publishing for a creator, brokering purchases and keeping the library for a holder (§ 5.3.11 Identity Log, § 7 Protocol Flows). An identity is its own DID and depends on no host; the relationship is a convenience, and an identity may change it or hold none.
- Host
-
A server that serves identity logs and executes sales: it sells and serves on creators' behalf under their delegations, and may be the home host of creators and holders (§ 8 HTTP API).
- Key event
-
A signed record by which an identity’s keys change under its unchanging DID; rotation and recovery are performed by key events, without an external directory (§ 4.4 Key Events (Rotation and Recovery)).
- Manifest (
fondachi/manifest) -
The signed record of one revision of a work: the work it belongs to, its position in that work’s sequence, its files, their chunks, and the parameters under which those chunks are encrypted. There is no separate revision object; the manifest is the revision record (§ 5.3.1 ContentManifest (fondachi/manifest), § 6.4.3 Chunk Encryption).
- Mirror
-
A server that stores content and serves it to holders of a valid license. Every host is a mirror of its own content (§ 8 HTTP API).
- Offer (
fondachi/offer) -
A signed statement of the terms on which a subject may be licensed: price, model, and validity period (§ 5.3.5 Offer (fondachi/offer)).
- Registrar
-
The capacity in which an identity’s home host orders its identity log: accepting registrations, assigning their positions, and signing the head that attests them (§ 5.3.11 Identity Log, § 4.4.6 The Home Host as Registrar). A capacity of the home host, not a distinct role: whoever serves the log holds it.
- Release (
fondachi/release) -
Public catalog metadata naming one or more works, in order: the durable identity of what is sold, and what a purchase offer prices. A work may appear in more than one release (§ 5.3.3 Release (fondachi/release)).
- Retrieval proof
-
A short-lived, mirror-bound credential minted by the current grant holder’s key, presented to obtain content (§ 5.4 Retrieval Proofs, § 7.4 Retrieval).
- Revision
-
A numbered position in a work’s sequence, occupied by exactly one manifest. A work’s identity is stable across revisions; its bytes are not (§ 5.3.1 ContentManifest (fondachi/manifest)).
- Seal boundary
-
The environment a sealer runs in, in which the operator can neither observe the content key nor alter the policy applied to it. Self-custody by the creator, an attested trusted execution environment, and threshold or multi-party computation each satisfy this; the protocol requires the property, not any particular mechanism (§ 6.5 Key Release and the Seal Boundary, § 10.7 The Active-Custodian Problem (Operator Forgery)).
- Sealer
-
The party that unwraps a content key and re-seals it to a grantee, at purchase or on redemption, always within a seal boundary (§ 6.5 Key Release and the Seal Boundary, § 7.2 Purchase).
- Stream (
fondachi/stream) -
A stable identity for a sequence of releases a creator publishes over time, and what the stream offer models price. A stream may cover the whole catalog or any slice of it; releases join a stream by creator-signed entries, and a release may belong to any number of streams (§ 5.3.4 Stream (fondachi/stream) and StreamEntry (fondachi/stream-entry)).
- Subject
-
What an offer prices, or what a grant conveys. An offer’s subject is one or more releases, except under the stream offer models, whose subject is one or more streams; a grant’s subject is one or more works. A release is therefore what an offer prices, a stream what a subscription covers, and a work what a grant conveys; a sale over a release conveys grants over the works it contains (§ 5.3.5 Offer (fondachi/offer)).
- Work (
fondachi/work) -
A stable lineage identity for a single piece of content, and what a grant conveys. A work carries no title or media type of its own; its textual presentation comes from a release that contains it (§ 5.3.2 Work (fondachi/work)).