docs: describe the minimal invitation and where its security comes from
CodeQL Analysis / Analyze CodeQL (push) Waiting to run
Deploy Application / deploy (push) Waiting to run
Mirror to Codeberg / mirror (push) Waiting to run
Mirror to PrivacyGuides / mirror (push) Waiting to run

The README, ARCHITECTURE.md and CRYPTOGRAPHY.md still described the old
handshake: keys and a session salt travelling inside the invitation, and a safety
code derived from the two DTLS fingerprints. None of that has been true since
5.9.0.

Adds a "The invitation" section to the README covering what the exchange was
reduced to and why that is a security change and not only a smaller QR code:
less material exposed before anyone is authenticated, the DTLS fingerprint as the
anchor, substituted keys failing closed on the commitment instead of on a human
comparison, a safety code that now covers the whole transcript rather than two
fingerprints, and the plain fact that a single QR is scanned in person where a
four-frame animated one pushes people to paste the invitation through a chat app.

Session lifecycle in ARCHITECTURE.md gains the in-band key exchange as its own
step. CRYPTOGRAPHY.md now states that the salt is derived from the transcript
rather than transmitted, and describes the transcript SAS and the signature that
replaced the challenge/response. DESCRIPTOR-SBQ2.md is listed in the doc index
and in the CONTRIBUTING impact table.
This commit is contained in:
lockbitchat
2026-08-06 18:53:51 -04:00
parent 32b642f0e6
commit 808fd99b73
8 changed files with 157 additions and 62 deletions
+35 -11
View File
@@ -28,9 +28,12 @@ handle; those are raw bytes and are overwritten when finished with.
## Session establishment
A session begins with one ECDH exchange. The public keys travel inside signed
packages, and the receiving side validates the SPKI structure (algorithm OID,
curve, point format and length) before importing anything.
A session begins with one ECDH exchange. The public keys do **not** travel in the
invitation — they are sent over the data channel once it opens, and are checked
against a 16-byte commitment carried in the invitation before they are parsed or
imported. The receiving side then validates the SPKI structure (algorithm OID,
curve, point format and length) before importing anything. See
[DESCRIPTOR-SBQ2.md](DESCRIPTOR-SBQ2.md) for the wire format and the reasoning.
From the shared secret, HKDF-SHA256 derives five independent values, each under
its own `info` label so that recovering one reveals nothing about the others:
@@ -47,15 +50,31 @@ The raw ECDH output is produced with `deriveBits`, used as HKDF input material,
and the buffer holding it is overwritten as soon as derivation completes. It is
never exported through an extractable key.
The 64-byte session salt is generated by the inviting peer and travels in the
invitation, so both sides derive the same schedule.
The 64-byte session salt is **not transmitted**. Both sides derive it as
SHA-512 of the handshake transcript — both invitations byte for byte, and both
key blobs, each length-prefixed. That has two consequences: the salt cannot be
steered by either side alone, and every key in the schedule is bound to both DTLS
fingerprints and every ICE candidate that was exchanged.
## Verification
Both peers compute the same safety code with HKDF, from the ECDH-derived key
fingerprint together with both DTLS fingerprints. The fingerprints are
canonicalised and sorted so that each side reaches the same value regardless of
role.
Both peers compute the same safety code with HKDF-SHA256, using the raw ECDH
shared secret as input material and the SHA-256 of the handshake transcript as
salt. The transcript covers **both invitations verbatim** — version byte, flags,
expiry, fingerprints, ICE credentials, every candidate, the commitments — and
**both key blobs**, each with a length prefix so no field boundary can be shifted.
Components are ordered by role rather than by who is computing, so both sides
reach the same seven digits.
Because the shared secret is the input material, an attacker who observes the
entire transcript still cannot predict the digits. Because the transcript is the
salt, nothing exchanged anywhere in the handshake, in either direction, can be
altered without changing them.
Possession of the identity key is proved separately: each side signs the
transcript with its ECDSA key and sends the signature over the channel. This
replaced an earlier challenge/response that echoed a nonce back across seven
fields; one signature binds the whole handshake at once.
Users compare the code through a channel an attacker cannot impersonate and enter
it manually. Local success is not sufficient: the session becomes verified only
@@ -63,8 +82,13 @@ after both peers confirm. Three incorrect entries end the session.
This is the step that makes the rest meaningful. Completing the key exchange
proves only that someone completed it; anyone able to rewrite the invitation can
do that with both people at once. The safety code covers the keys actually in
use, so a substitution changes the code the users read to each other.
do that with both people at once. The safety code covers the keys actually in use
and the invitations they arrived with, so a substitution anywhere changes the code
the users read to each other.
Key substitution alone — an attacker who can rewrite the in-band blob but not the
invitation — does not get that far: the commitment check fails first and the
connection is closed without anyone comparing anything.
Verification is also a gate rather than a label. Before it completes, the session
declines to act on control messages from the peer: reconnection signalling, call