Contact Voucher Protocol Narration

Introduction

The voucher spec was very difficult for me to read and it overloads variable names and doesn’t make certain facts explicit enough. I wrote this spec for my own understanding of the design as an implementation guide for myself.

Streams

There is no separate reply stream. Alice replies on the very same stream Bob used to mint the voucher.

  • MessageStream: Bob writes; the group reads. This is the stream whose live messages the protocol must protect.
  • VoucherStream: the rendezvous, derived entirely from the Voucher. Box 0 holds Bob’s VoucherPayload; box 1 holds Alice’s VoucherReply.
  • the group’s other streams: each existing member has their own MessageStream, just like Bob’s.

Stream Objects

  • MessageStream: .WriteCap (Bob keeps), .ReadCap (travels in VoucherPayload, inside SignedPleaseAdd).
  • VoucherStream: read and write caps derivable by anyone holding the Voucher.
  • VoucherKeypair: VoucherSecretKey (Bob keeps), VoucherPublicKey (travels in VoucherPayload). This is the MKEM keypair under which Alice seals the reply to Bob.

Messages

  • SignedPleaseAdd is Bob’s signed self-introduction: a serialized PleaseAdd (his DisplayName and MessageStream.ReadCap) plus a signature over it produced by MessageStream.WriteCap.
// PleaseAdd is a member's request to join, carrying their display name
// and the read cap that lets others read their messages.
type PleaseAdd struct {
    // DisplayName is the party's name to be displayed in chat clients.
    DisplayName string

    // UniversalReadCap is the BACAP read cap for this member's
    // MessageStream, letting others read all messages posted by them.
    UniversalReadCap *bacap.UniversalReadCap
}

// SignedPleaseAdd binds a PleaseAdd to a signature made by the member's
// write cap, so any party can verify the name-and-cap binding.
type SignedPleaseAdd struct {
    // PleaseAdd contains the CBOR serialized PleaseAdd struct.
    PleaseAdd []byte

    // Signature contains the cryptographic signature over the PleaseAdd field.
    Signature []byte
}
  • WhoReply := the existing members’ MessageStream read caps (one or more), each unchanged and paired with a start index, so Bob can read everyone already in the group. Each index is the box after Alice’s last published acknowledgement of that stream; Alice’s own starts at her Introduction of Bob.
  • VoucherReply := WhoReply || VoucherSalt, MKEM-sealed to VoucherPublicKey and written to VoucherStream box 1. Box 1 is an ordinary BACAP box, so the payload is BACAP-encrypted as a stream box and then additionally MKEM-encrypted to Bob’s VoucherPublicKey; only Bob, holding VoucherSecretKey, can open it.
  • Introduction := Bob’s display name, his MessageStream read cap unchanged, and his salted start index, published to the existing group so the members can read Bob. The group receives the salted index directly and never the salt.
VoucherPayload := SignedPleaseAdd || VoucherPublicKey
Voucher        := Hash(VoucherPayload)

The VoucherSalt: re-seeding the MessageStream

VoucherSalt is the heart of the protocol. It is not a nonce and it does not modify BACAP. It is a value that replaces the KDF state inside a MessageStream’s MessageBoxIndex, which re-seeds the index chain and so moves the stream to a fresh, unpredictable sequence of box IDs. Only two parties ever touch it. Bob mutates his MessageStream.WriteCap by the salt and writes his real messages at the mutated position. Alice applies the same salt once to the index she pairs with Bob’s MessageStream.ReadCap, leaving the read cap itself unchanged, so writer and readers meet at the mutated position. The group never learns the salt; it only ever receives the already-salted index.

That is why the spec says derive Bob’s read capability from VoucherPayload + VoucherSalt: the read cap published in box 0 is only half of it. That derivation is Alice’s alone. The read cap in box 0 names Bob’s original stream; the salted index says where Bob’s real messages actually go. She performs the derivation once and hands the result to the group.

The salt is the one secret a voucher snoop never receives. It is born at induction, not at mint: Alice mints it and seals it inside the MKEM-encrypted VoucherReply. It is never written to box 0, so Bob’s PleaseAdd carries only his original read cap, never the salt. An interceptor of the out-of-band Voucher can derive the VoucherStream, read box 0, and check that the voucher was spent, but he holds only the read cap and its unsalted index. He cannot compute the salted box IDs, so he cannot read a single one of Bob’s actual messages. That is exactly the spec’s stated confidentiality goal.

Because the salt re-seeds rather than decorates the stream, it is a per-member fact known only to that member and to whoever inducted them. A mature group mixes voucher-joined members (each on their own salt-mutated index) with seed members (on the unmutated index), and no member needs to carry any salt: every index that circulates beside a read cap is already salted. WhoReply hands Bob the existing members’ read caps with their start indexes, and the Introduction hands the group Bob’s read cap with the index Alice salted.

Steps

  1. Bob mints. He generates his MessageStream (keeping MessageStream.WriteCap) and a VoucherKeypair (keeping VoucherSecretKey). He forms SignedPleaseAdd = {DisplayName, MessageStream.ReadCap} signed by MessageStream.WriteCap; sets VoucherPayload := SignedPleaseAdd || VoucherPublicKey; Voucher := Hash(VoucherPayload).
  2. Bob publishes. The Voucher derives the VoucherStream; Bob writes VoucherPayload to box 0.
  3. Bob → Alice (OOB). He hands over only the Voucher.
  4. Alice reads and verifies. From Voucher she derives the VoucherStream, reads box 0, checks Hash(VoucherPayload) == Voucher, and verifies the SignedPleaseAdd signature against its read cap’s rootPK.
  5. Alice replies. She mints VoucherSalt, assembles WhoReply from the start positions above, forms VoucherReply := WhoReply || VoucherSalt, and MKEM-seals it to VoucherPublicKey. She publishes no acknowledgements between this step and the commit.
  6. Alice commits (all-or-nothing COPY). She first derives Bob’s start index by applying the VoucherSalt to his read cap’s index, leaving the read cap unchanged. Then, in one operation: write the sealed VoucherReply to VoucherStream box 1; publish the Introduction (Bob’s display name, his read cap, and his salted start index) to her group, which never sees the salt itself; tombstone box 0 against reuse. Because no acknowledgement separates the start positions from this commit, there is no race: every member keeps each box Alice had not acknowledged at the Introduction until Bob acknowledges it (see Joining in the group chat spec). She rewrites box 1 each replica epoch until Bob’s first message appears, for at most the Sent-box retention window; a Bob later than that needs a new voucher.
  7. Bob finishes. He polls VoucherStream box 1, MKEM-opens the VoucherReply with VoucherSecretKey, and recovers WhoReply and VoucherSalt. He mutates his MessageStream.WriteCap by the salt and begins writing real messages there. The group already holds his read cap and salted start index, so they read him without ever learning the salt. Both sides now share the live streams.