Skip to main content

Sharing documents

A holder who has opened a document with their credential package can give someone else access to it without any server holding a key. There are two ways:

WayReachesCan be stopped
Shared document file (a grant), sealed to the recipient's share codeOnly the wallet that made that share codeNot for what was already opened; the issuer can replace the key for the future
Capability link with the key in the URL fragmentAnyone who gets the linkOnly by the issuer replacing the key

When a key must be withdrawn, the issuer re-encrypts the document under a new key, gives the holder a key update file, and removes the old stored copies. The file formats are described in Sharing files and codes.

Sharing a document is not proving a credential

Sharing gives someone the document. To prove the credential itself, the holder gives the credential id or link (anyone can verify it), the package (to prove name and ID), or a disclosure file for chosen fields.

Preconditions​

  • v0.3 and a version 2 package with a cid and a documentKey. A package without a protected document gives "This credential has no protected document to share. Documents stored before encryption was added cannot be shared this way."
  • The holder has opened the document on /student (Open a document), which shows Share this document after a successful open.
  • For a grant, the recipient has a plain wallet to make a share code. Contract wallets cannot derive document keys.

Sharing with one wallet​

  1. The recipient makes a share code. On /student, panel My share code, Show my share code asks for two signatures (the same message twice) and shows Your share code with Copy code and a fingerprint. The panel says: "Give this code to a person who wants to share a document with you. It is safe to send: it only lets them lock a document for your wallet. Read the fingerprint to them another way so they can check it is yours."
  2. The holder seals the key. Under Share this document ("Ask the person for their share code (they make it on their Certichain page). The file you create can be opened only by the wallet that made that code. They keep what they read, and you cannot take it back."), the holder pastes the code into Recipient's share code. A valid code shows "Check this with the person:" and the fingerprint. Create shared file seals the document key to the code's public key with HPKE (no wallet needed) and saves shared-document-<8 hex>.json: "Shared file saved. Send it to the person; only the wallet that made that share code can open it."
  3. The recipient opens it. On /student, panel Open a shared document ("Your wallet asks for two signatures so this page can work out your key; they do not send anything."), field Shared document file, Open document. DocumentService.openGrant checks the file's digest, then asks the chain: the credential must exist and its anchored issuer and document hash must equal the file's, before the wallet is asked for anything. Then the wallet derives its key, which must match the grant's kid, and the container is fetched, checked against the issuer, decrypted and hashed.

The result card says "Decrypted in this browser. It matches the document hash recorded on the blockchain for this credential.", "Issued by 0x..., the address the blockchain records for this credential.", and links to Check that this credential is still active.

Create a link opens a confirmation, Create a link to this document?: "Anyone who gets the link can read the document. The link cannot expire or be limited to one person. To stop it, ask the issuer to replace the key; copies already saved are not affected." Create link shows Link to share with Copy link.

https://<site>/open#<credential id>.<cid>.<document key>

The /open page ("Shared with you", Open a shared document: "The key in this link is used only in this browser. The document is downloaded, decrypted here, and checked against the credential on the blockchain before you can save it.") takes the fragment once, clears it with history.replaceState, and keeps it in memory only. The route sends Referrer-Policy: no-referrer and Cache-Control: no-store. The link has no signature, so the chain decides whether the document belongs to a real credential.

ProblemMessage
No fragment (cut short, or reloaded after it was cleared)"This page opens a document from a link that carries its key. The address has no key (the link was cut short, or the page was reloaded). Open the link again."
Malformed link"This link is incomplete or damaged. Ask for a new one."
Credential not on this network"No credential with this id was found on this network, so the document was not opened."
Chain or gateway unreachableThe message plus Try again

Replacing a document key​

An issuer replaces a document's key when a key leaked, a link must stop working, or a grant should no longer open future downloads. The credential does not change: same id, same document hash, same status.

  1. On the dashboard, a row's Replace key opens Replace this document's key?: "The document is encrypted again under a new key and stored as a new copy. You then get a key update file to send to the holder, and can remove the old copies." and "The old key stops opening the document once the old copies are removed. Someone who already saved the document or the old key keeps what they have." Button Replace key.
  2. replaceDocumentKey (src/app/dashboard/replace-document-key.ts) first looks up the issuer's other credentials that anchor the same file (by document hash). If they cannot be read, nothing is replaced, because removal could not later tell their copies from this one's.
  3. DocumentService.rotate opens the newest copy the issuer can open, encrypts the same file again under a fresh document key, uploads it, and returns the new CID, a key update, and the list of older copies. Nothing is removed in this step, so a failure leaves the old copy working ("The key could not be replaced. Nothing was changed.").
  4. The panel The new copy is stored under a new key. shows two steps:
    • Hand the holder the key update: Save key update file saves key-update-<8 hex>.json. "It is secret like the key itself. The holder applies it to their credential package; until they do, their old package opens nothing once the old copies are gone."
    • Remove the old copies: Remove old stored copies (N), enabled only after the key update file was saved ("Save the key update file first."). The result: "Removed N old copies from storage. A copy someone already saved elsewhere is not affected."
  5. If other credentials of this issuer use the same file, removal is not offered: "N other credentials you issued use the same file. Their stored copies cannot be told apart from this one's, so the old copies are kept."

Removal on the server​

DELETE /api/documents?documentHash=...&keep=<cid>&issuer=... requires the issuer session, refuses a query that names another wallet, and has its own budget (20 removals per wallet per hour). It lists the issuer's pins of that document hash, newest first, requires the kept CID to be among them (409 KEEP_NOT_FOUND: "The copy to keep is not stored under this account. Nothing was removed."), and unpins only the copies older than the kept one. It never removes the last copy and never touches another issuer's pins.

The holder applies the update​

On /student, panel Open a document, after choosing the package: "Did the issuer send you a key update? Apply it first. It replaces the stored copy this package points at." The holder chooses Key update file. The app builds a new package with the new cid and documentKey, opens the document with it to prove it works, and only then saves it as credential-<8 hex>.json: "Package updated and saved. The new package opened the document. Keep the old file until you rely on the new one: the old one stops opening the document once the issuer removes the old copies." A key update for another credential gives "This key update belongs to a different credential than this package."

Wallet prompts​

WhoActionPrompts
RecipientShow my share code2 signatures (once per session)
HolderCreate shared file, Create a linkNone
RecipientOpen document (grant)2 signatures (once per session)
AnyoneOpening a linkNone
IssuerReplace key2 signatures per key version tried (once per session), a sign-in message if needed
IssuerRemove old stored copiesA sign-in message if the session expired
HolderApplying a key updateNone

What sharing cannot do​

  • Take back what a recipient already opened or saved.
  • Expire a link or limit it to one person.
  • Tell the holder who opened a link.
  • Work for documents stored before encryption (version 1 packages).

Code map​

ConcernFile
Servicesrc/services/domain/document-service.ts: myShareCode, grantAccess, capabilityLink, openGrant, openCapability, rotate, removeOldCopies
Formatssrc/lib/doc-crypto/share-code.ts, grant.ts, capability.ts, key-update.ts
Holder panelssrc/app/student/SharePanel.tsx, MyShareCodePanel.tsx, SharedDocumentPanel.tsx, ApplyKeyUpdate.tsx, OpenDocumentPanel.tsx, OpenedDocumentNotice.tsx
Link pagesrc/app/open/OpenLinkContent.tsx, src/lib/security-headers.ts, next.config.ts
Issuer flowsrc/app/dashboard/ReplaceKeyFlow.tsx, replace-document-key.ts
Serversrc/app/api/documents/route.ts, src/lib/server/pinata-unpin.ts, src/services/document-api.ts

Tests​

  • src/services/domain/document-service.sharing.test.ts, document-service.rotation.test.ts
  • src/lib/doc-crypto/share-code.test.ts, grant.test.ts, capability.test.ts, key-update.test.ts, access-matrix.test.ts
  • src/app/student/SharePanel.test.tsx, MyShareCodePanel.test.tsx, SharedDocumentPanel.test.tsx, ApplyKeyUpdate.test.tsx, src/app/open/OpenLinkContent.test.tsx
  • src/app/dashboard/ReplaceKeyFlow.test.tsx, replace-document-key.test.ts, src/app/api/documents/route.test.ts, src/lib/security-headers.test.ts
  • E2E: e2e/journeys/share-and-replace.journey.ts, replace-shared-file.journey.ts