docs: describe the minimal invitation and where its security comes from
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:
+33
-14
@@ -15,27 +15,46 @@ comparison is what closes that gap.
|
||||
## Session lifecycle
|
||||
|
||||
```text
|
||||
1. Invitation Peer A generates an ECDH key pair, an SDP offer and a session
|
||||
salt, and exports them as a single invitation.
|
||||
1. Invitation Peer A generates its key pairs and an SDP offer, and exports a
|
||||
compact descriptor: ICE candidates, the DTLS certificate
|
||||
fingerprint, an expiry, and a 16-byte commitment to its key
|
||||
material. 110-150 bytes; one QR code. No keys travel in it.
|
||||
|
||||
2. Response Peer B validates the invitation, derives the shared secret,
|
||||
and returns its own keys and SDP as a response.
|
||||
2. Response Peer B validates the descriptor strictly, answers the SDP, and
|
||||
returns a descriptor of the same shape, tagged so that it can
|
||||
only be an answer to this particular invitation.
|
||||
|
||||
3. Transport up DTLS completes and the data channel opens. At this point both
|
||||
sides hold session keys, but neither knows who the other is.
|
||||
3. Transport up DTLS completes and the data channel opens. Only the peer
|
||||
holding the private key behind the fingerprint in the
|
||||
invitation can reach this point.
|
||||
|
||||
4. Verification Both sides display the same safety code. The users compare it
|
||||
over a channel an attacker cannot impersonate and enter it.
|
||||
4. Key exchange Each side sends its public keys over the open channel as the
|
||||
first frame. Each verifies the other's blob against the
|
||||
commitment from the invitation BEFORE parsing it, then derives
|
||||
the session from a transcript of both descriptors and both
|
||||
blobs, and signs that transcript to prove it owns its identity
|
||||
key. Any failure closes the connection.
|
||||
|
||||
5. Verified Only now does the session accept traffic that changes state,
|
||||
5. Verification Both sides display the same safety code, derived from that
|
||||
transcript. The users compare it over a channel an attacker
|
||||
cannot impersonate and enter it.
|
||||
|
||||
6. Verified Only now does the session accept traffic that changes state,
|
||||
and only now does the chat open.
|
||||
```
|
||||
|
||||
Step 3 is the one worth dwelling on. Completing the handshake proves that someone
|
||||
performed a key exchange. It does not prove who. Anyone positioned on the
|
||||
out-of-band channel can substitute their own keys and complete step 3 with both
|
||||
people at once. Step 4 is the only step that distinguishes the intended peer, so
|
||||
everything that could be useful to an impostor waits for it.
|
||||
Steps 3 and 4 are the ones worth dwelling on. The fingerprint in the invitation
|
||||
authenticates the transport to whoever showed you the code, and the commitment
|
||||
means substituted key material is refused automatically rather than noticed by a
|
||||
human. But neither proves *who* showed you the code. Anyone positioned on the
|
||||
out-of-band channel can rewrite the whole invitation, commitment included, and
|
||||
complete steps 3 and 4 with both people at once. Step 5 is the only step that
|
||||
distinguishes the intended peer, so everything that could be useful to an
|
||||
impostor waits for it — and because the safety code is computed over the full
|
||||
transcript, a rewritten handshake cannot produce matching digits.
|
||||
|
||||
The invitation format and its decoder rules are in
|
||||
[DESCRIPTOR-SBQ2.md](DESCRIPTOR-SBQ2.md).
|
||||
|
||||
## What verification gates
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ When behaviour changes, update the documentation in the same commit:
|
||||
| --- | --- |
|
||||
| Anything user-visible | `README.md`, `CHANGELOG.md` |
|
||||
| Verification, keys, the ratchet | `doc/CRYPTOGRAPHY.md`, `doc/ARCHITECTURE.md` |
|
||||
| The invitation format or the in-band key exchange | `doc/DESCRIPTOR-SBQ2.md`, `doc/CRYPTOGRAPHY.md`, `doc/ARCHITECTURE.md` |
|
||||
| Deployment, ICE, file policy | `doc/CONFIGURATION.md` |
|
||||
| Calls, codecs, adaptation | `doc/CALLS.md` |
|
||||
| Internal interfaces | `doc/API.md` |
|
||||
|
||||
+35
-11
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ if you are looking for an overview or a quick start.
|
||||
| --- | --- |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | How a session is established, verified and torn down, and where each guarantee comes from |
|
||||
| [CRYPTOGRAPHY.md](CRYPTOGRAPHY.md) | Key schedule, the Double Ratchet, SAS verification, memory handling |
|
||||
| [DESCRIPTOR-SBQ2.md](DESCRIPTOR-SBQ2.md) | The invitation format: wire layout, why it is small, the in-band key exchange, decoder rules |
|
||||
| [CONFIGURATION.md](CONFIGURATION.md) | Deployment, ICE and TURN setup, privacy modes, file transfer policy |
|
||||
| [CALLS.md](CALLS.md) | Voice and video: codec choices, adaptation, and why each value was picked |
|
||||
| [API.md](API.md) | Internal interfaces of the WebRTC manager and file transfer system |
|
||||
|
||||
Reference in New Issue
Block a user