Envelope & content addressing
OC Chat does not restate OC Lock's cryptography — read the
OC Lock envelope reference first. OC Chat adds two kind
values and one cryptographic rule that differs from base OC Lock. That rule
is load-bearing, so this page covers it precisely.
The envelope kinds
OC Chat introduces new values for the OC Lock envelope kind field, alongside
the existing "identity" and "payment":
kind | Mode | New fields |
|---|---|---|
"chat" | speak-now / pay-to-reach | optional postage (§6) |
"chat-seal" | seal-til-block | seal (§7) |
"chat-channel" | public channel post | channel_id, write_proof |
A conforming OC Lock implementation that does not understand these kinds MUST reject them rather than mis-decrypt. The transport (§8) is OC Lock's gift-wrap, unchanged.
chat-channelis public. Unlikechat/chat-seal, a v1chat-channelpost has no recipient set, no key-wrapping, and no AEAD ciphertext over a body. Itsrecipients[]MUST be empty (E_CHANNEL_RECIPIENTSotherwise); the body is plaintext, content-addressed, and BIP-322-rooted by the author's device signature.
The recipient-exclusion rule (NORMATIVE)
This is the one cryptographic rule that differs from base OC Lock.
In base OC Lock, both the envelope id and the AEAD AAD include the
recipients[] identities. That makes the envelope un-re-keyable: changing
recipients[] after sealing changes the id (breaking the BIP-322 signature)
and the AAD (breaking the ciphertext tag). For a one-shot file drop that is fine
— but a seal beacon or a payment relay needs to re-wrap the content key for
the eventual recipient after the message was signed.
For kind ∈ { "chat", "chat-seal" }, recipients[] is delivery routing, not
content, and is excluded from both:
chat_aad = SHA-256( canonical( env | id="", ciphertext="", sig.value="", recipients=[] ) ) // 32 bytes, the GCM AAD
chat_envelope_id = SHA-256( canonical( env | id="", sig.value="", recipients=[] ) ) // hex, committed by sig
Consequences a conforming implementation MUST honor:
- The
idcommits to everything exceptrecipients[]:v,kind,alg,from,ciphertext,nonce_ct,hint,created_at,expires_at,payment,postage,seal. Changing any of these changes theid. - The sender's
sig.valueis BIP-322 overchat_envelope_id. - A holder MAY replace
recipients[]entirely (a re-wrap) and theid,sig, and ciphertext tag remain valid. The re-wrap output is a detachedrecipients[]entry the client merges locally; it MUST NOT recompute the signedid. - The ciphertext AAD is
chat_aad, which is also recipient-independent, so the GCM tag survives a re-wrap.
recipients[] entries are otherwise structured exactly as OC Lock (address,
device_id, device_pk, eph_pk, wrapped_key, nonce_kek) and sorted by
device_id in any canonical form that includes them (wire interop), even though
they do not enter the id.
Why this matters — the re-wrap path
rendering diagram…
Test vector vc04-seal-rewrap-stability proves this end to end: a chat-seal
envelope sealed to a beacon device is re-wrapped to a recipient device, and
id_after == id_before while the recipient's ciphertext tag verifies. This is
the whole reason chat-kind id/AAD exclude recipients[] — it is what
makes the seal release and
payment-mode re-wrap round-trippable. An adversarial review
caught that the first design, which kept recipients[] in the id, would have
shipped un-round-trippable sealed messages
(Why).
Versioning
OC Chat versions with OC Lock's envelope.v (currently 2). New kind values
and new optional blocks (postage, seal sub-fields) are additive: clients
MUST preserve unknown fields when relaying and MAY ignore them when decrypting.
Incompatible changes increment envelope.v.
Next
- Threading & attachments — what rides inside the ciphertext.
- Seal-til-block — the re-wrap release in practice.
- Specification — the full normative envelope rules and error codes.