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’sVoucherPayload; box 1 holds Alice’sVoucherReply. - the group’s other streams: each existing member has their own MessageStream, just like Bob’s.
Stream Objects
- MessageStream:
.WriteCap(Bob keeps),.ReadCap(travels inVoucherPayload, insideSignedPleaseAdd). - VoucherStream: read and write caps derivable by anyone holding the
Voucher. - VoucherKeypair:
VoucherSecretKey(Bob keeps),VoucherPublicKey(travels inVoucherPayload). This is the MKEM keypair under which Alice seals the reply to Bob.
Messages
SignedPleaseAddis Bob’s signed self-introduction: a serializedPleaseAdd(hisDisplayNameandMessageStream.ReadCap) plus a signature over it produced byMessageStream.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), so Bob can read everyone already in the group.VoucherReply:=WhoReply || VoucherSalt, MKEM-sealed toVoucherPublicKeyand 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’sVoucherPublicKey; only Bob, holdingVoucherSecretKey, can open it.Introduction:= Bob’s display name together with his salt-mutated MessageStream read cap, published to the existing group so the members can read Bob. The group receives the mutated cap 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 mutates Bob’s matching MessageStream.ReadCap by the same salt once, before she shares it, so writer and readers meet at the mutated position. The group never learns the salt; it only ever receives the already-mutated read cap.
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 salt re-seeds it to 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 un-mutated read cap. He cannot compute the salt-mutated 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 read cap that circulates is already at its live, mutated position. WhoReply hands Bob the existing members’ live read caps directly, and the Introduction hands the group Bob’s read cap after Alice has applied his salt.
Steps
- Bob mints. He generates his MessageStream (keeping
MessageStream.WriteCap) and aVoucherKeypair(keepingVoucherSecretKey). He formsSignedPleaseAdd = {DisplayName, MessageStream.ReadCap}signed byMessageStream.WriteCap; setsVoucherPayload := SignedPleaseAdd || VoucherPublicKey;Voucher := Hash(VoucherPayload). - Bob publishes. The
Voucherderives the VoucherStream; Bob writesVoucherPayloadto box 0. - Bob → Alice (OOB). He hands over only the
Voucher. - Alice reads and verifies. From
Vouchershe derives the VoucherStream, reads box 0, checksHash(VoucherPayload) == Voucher, and verifies theSignedPleaseAddsignature against its read cap’s rootPK. - Alice replies. She mints
VoucherSalt, assemblesWhoReplyfrom the existing members’ live read caps, formsVoucherReply := WhoReply || VoucherSalt, and MKEM-seals it toVoucherPublicKey. - Alice commits (all-or-nothing COPY). She first mutates Bob’s published read cap by the
VoucherSalt. Then, in one operation: write the sealedVoucherReplyto VoucherStream box 1; publish theIntroduction(Bob’s display name and his salt-mutated read cap) to her group, which never sees the salt itself; tombstone box 0 against reuse. - Bob finishes. He polls VoucherStream box 1, MKEM-opens the
VoucherReplywithVoucherSecretKey, and recoversWhoReplyandVoucherSalt. He mutates hisMessageStream.WriteCapby the salt and begins writing real messages there. The group already holds his mutated read cap, so they read him without ever learning the salt. Both sides now share the live streams.