Skip to main content

Sharing files and codes

Four small formats move access to an encrypted document between people without any server holding a key. They are produced and read on the student page, the /open page and the issuer dashboard. The flow that uses them is described in Sharing documents.

FormatWho makes itWho reads itSecret?
Share codeA recipient, from their walletThe holder, to seal a key to that recipientNo. It is a public key.
Shared document fileThe holderThe one recipient whose share code was usedOpens only for that wallet
Capability linkThe holderAnyone who gets the linkYes. The key is in the link.
Key update fileThe issuer, after replacing a document keyThe holder, to update their packageYes. It carries the new key.

Share code​

A share code is the recipient's X25519 public key (the same wallet-derived key described in Encrypted document container) with a version byte and a checksum, in one copyable string.

ccshare1_ + base64url( version (1 byte) ‖ public key (32 bytes) ‖ checksum (4 bytes) )

checksum = first 4 bytes of SHA-256( "certichain.share.v1" ‖ version ‖ public key )
fingerprint = kid of the public key (16 hex characters) in groups of four
PartValue
Prefixccshare1_
Version1
Body length37 bytes before base64url
Made on/student, panel My share code, button Show my share code (two wallet signatures the first time in a session)
Shown withThe fingerprint, for example 0a1b 2c3d 4e5f 6071, to read out over another channel

The checksum catches typing and copying mistakes. It does not prove whose key it is: the holder compares the fingerprint the share panel shows ("Check this with the person: ...") with the one the recipient reads out.

Parse errorMessage
Missing prefix"This is not a share code."
Wrong length or character"This share code is incomplete or contains a wrong character. Copy the whole code again."
Other version"This share code was made by a newer version of the app."
Checksum mismatch"This share code has a typing mistake. Copy the whole code again."

Shared document file​

A shared document file (a "grant") gives one recipient the key to one stored document. It carries the signed credential fields, so the recipient can see who issued it, and the document key sealed to the recipient's public key with HPKE. It carries no holder details and no key in the clear.

PropertyValue
File nameshared-document- followed by the first 8 hex characters of the credential id, .json
Made on/student, panel Open a document, after a successful open: Share this document, field Recipient's share code, button Create shared file
Opened on/student, panel Open a shared document, field Shared document file, button Open document
Size accepted64 KiB
FieldTypeMeaningValidation
typestring"certichain-grant"Exact value; another type is refused with the kind it looks like
versionnumber1Exact value
domain, credential, signature, credentialIdThe signed credential fields, as in the credential packagereadSignedFields
cidstringIPFS CID of the containerValid CID
grant.kidstringThe recipient key id16 hex characters
grant.encstringHPKE encapsulated keyBase64url, 32 bytes
grant.ctstringThe sealed document keyBase64url, 48 bytes

Any field outside this list, including a documentKey, is refused. The wrapping uses the same HPKE suite and the same info binding ("certichain.dek.v1" ‖ document hash) as the container's recipient entries.

Opening checks, in order (DocumentService.openGrant):

  1. The signed fields still hash to credentialId; otherwise "This credential package is damaged, was changed after it was signed, or does not match the stored document, so it was not opened."
  2. The chain is read: the credential must exist and its anchored issuer and document hash must equal the file's. Anyone can build a grant that agrees with itself, so the chain decides before the wallet is asked for anything.
  3. The wallet derives its key (two signatures) and must own grant.kid; otherwise "Your account's keys do not open this document. It may have been stored with an earlier key or by another account."
  4. The container is fetched, its header issuer must equal the credential's issuer, and the decrypted file must match the anchored hash.

The result shows "Issued by ..., the address the blockchain records for this credential." and a link Check that this credential is still active.

A capability link carries the document key in the URL fragment. The fragment is never sent to a server, but anyone who has the link can read the document.

https://<site>/open#<credential id, lowercase>.<cid>.<base64url document key>
PartRule
OriginThe site's own origin, http or https only
Credential id0x plus 64 hex characters, written in lowercase
CIDA valid CID
KeyBase64url of exactly 32 bytes

Anything else gives "This link is incomplete or damaged. Ask for a new one."

The holder must confirm a dialog first (Create a link to this document?, button Create link): "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."

The /open page reads the fragment once, removes it from the address bar with history.replaceState, and keeps it only in memory. The route is served with Referrer-Policy: no-referrer and Cache-Control: no-store (OPEN_PAGE_HEADERS in src/lib/security-headers.ts). Because a link has no signature, the chain decides: the credential must exist and the container's header must name its issuer, and the decrypted file must match the anchored hash, before anything is shown.

Key update file​

After an issuer replaces a document's key, the holder needs the new copy's address and the new key. The key update file carries exactly that. The signed credential fields do not change, so the credential id stays the same.

PropertyValue
File namekey-update- followed by the first 8 hex characters of the credential id, .json
Made onIssuer dashboard, row action Replace key, then Save key update file
Applied on/student, panel Open a document, "Did the issuer send you a key update?", field Key update file
Size accepted16 KiB
FieldTypeMeaningValidation
typestring"certichain-key-update"Exact value
versionnumber1Exact value
credentialIdstringThe credential the update belongs to32-byte hex
cidstringCID of the new containerValid CID
documentKeystringBase64url of the new 32-byte keyExactly 32 bytes

Extra fields are refused. Applying an update to a package of another credential fails with "This key update belongs to a different credential than this package." Applying it creates a new package (version 2, new cid and documentKey), opens the document with it to prove it works, and only then saves it as credential-<8 hex>.json. If the new package does not open, nothing is changed.

Examples​

Fake values throughout.

ccshare1_AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
{
"type": "certichain-grant",
"version": 1,
"domain": { "name": "Certichain", "version": "1", "chainId": "11155111", "verifyingContract": "0x2222222222222222222222222222222222222222" },
"credential": {
"issuer": "0x1111111111111111111111111111111111111111",
"subjectCommitment": "0x0000000000000000000000000000000000000000000000000000000000000a01",
"documentHash": "0x9f2c4d0000000000000000000000000000000000000000000000000000000001",
"metadataHash": "0x0000000000000000000000000000000000000000000000000000000000000b02",
"recordType": 1,
"issuedAt": 1792800000,
"validUntil": 0,
"nonce": "7382917462837461"
},
"signature": "0x<65-byte signature>",
"credentialId": "0x0000000000000000000000000000000000000000000000000000000000000c03",
"cid": "bafybeiexampleexampleexampleexampleexampleexampleexampleexa",
"grant": {
"kid": "0a1b2c3d4e5f6071",
"enc": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"ct": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
}
{
"type": "certichain-key-update",
"version": 1,
"credentialId": "0x0000000000000000000000000000000000000000000000000000000000000c03",
"cid": "bafybeinewcopyexampleexampleexampleexampleexampleexampleexa",
"documentKey": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
}

Source files​

  • src/lib/doc-crypto/share-code.ts: encodeShareCode, parseShareCode, shareFingerprint
  • src/lib/doc-crypto/grant.ts: buildGrant, parseGrant, unwrapGrant
  • src/lib/doc-crypto/capability.ts: buildCapabilityFragment, parseCapabilityFragment
  • src/lib/doc-crypto/key-update.ts: buildKeyUpdate, parseKeyUpdate, applyKeyUpdate
  • src/lib/doc-crypto/file-kind.ts: type values and wrong-file detection
  • src/lib/doc-crypto/errors.ts: ShareCodeError, CapabilityLinkError, KeyUpdateMismatchError, WrongFileKindError
  • src/services/domain/document-service.ts: myShareCode, grantAccess, capabilityLink, openGrant, openCapability, rotate
  • src/lib/security-headers.ts: headers for /open

Tests​

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