{
    "componentChunkName": "component---src-templates-protocol-protocol-tsx",
    "path": "/webrtc/1.0/",
    "result": {"data":{"markdownRemark":{"html":"<h2 id=\"Summary\">Summary</h2>\n<p>The WebRTC 1.0 protocol enables secure video and audio communication using <strong>DIDComm v2</strong> as the signaling channel. It supports two topologies: <strong>peer-to-peer mesh</strong> for small calls (2–8 participants) and <strong>SFU-mediated</strong> for scalable calls (10–100+ participants). The protocol provides NAT traversal, <strong>binds WebRTC DTLS certificates to DID keys</strong> for MITM protection, supports <strong>end-to-end encryption (E2EE) with SFrame</strong> in SFU mode, and works seamlessly with DIDComm mediators and <strong>Message Pickup</strong> for offline scenarios.\nThis draft additionally defines <strong>Out-of-Band (OOB) Join Links</strong> so users can connect by clicking a link (or scanning a QR) delivered by any channel (email/SMS/DM) <strong>without requiring a mediator for the first hop</strong>—while keeping all subsequent signaling DID-authenticated and encrypted.</p>\n<hr>\n<h2 id=\"Goals\">Goals</h2>\n<ul>\n<li>Enable both P2P mesh and SFU-mediated topologies in one protocol</li>\n<li>Support ~2–8 participants via mesh, ~10–100+ via SFU</li>\n<li>Use DIDComm v2 <strong>authcrypt</strong> for signaling</li>\n<li>Bind identity to media streams via DID-signed certificate fingerprints</li>\n<li>Maintain E2EE in SFU mode using <strong>SFrame</strong> (Insertable Streams)</li>\n<li>Support privacy controls (relay-only mode, pairwise DIDs)</li>\n<li>Handle glare resolution, renegotiation, and ICE restart</li>\n<li>Work with standard DIDComm mediators (mesh) or SFU-capable mediators (SFU)</li>\n<li><strong>Provide link-native, mediator-free onboarding</strong> via OOB Join Links (QR/URL)</li>\n</ul>\n<hr>\n<h2 id=\"Roles\">Roles</h2>\n<ul>\n<li><strong>caller</strong>: Initiates peer-to-peer WebRTC sessions</li>\n<li><strong>callee</strong>: Receives peer-to-peer session proposals</li>\n<li><strong>host</strong>: Creates multi-party rooms and invites participants</li>\n<li><strong>participant</strong>: Joins multi-party sessions</li>\n<li><strong>sfu-mediator</strong>: DIDComm mediator with SFU media forwarding capabilities</li>\n</ul>\n<h3 id=\"Role-Requirements\">Role Requirements</h3>\n<p><strong>Standard DIDComm mediators</strong> support mesh topology with no additional requirements beyond Message Pickup and Coordinate Mediation.</p>\n<p><strong>sfu-mediator</strong> implementations MUST:</p>\n<ul>\n<li>Support WebRTC media forwarding (receive from all, forward to all)</li>\n<li>Manage room lifecycle (create, join, leave, destroy)</li>\n<li>Forward <strong>SFrame-encrypted</strong> frames without decrypting</li>\n<li>Provide TURN infrastructure for NAT traversal</li>\n<li>Implement quality adaptation (simulcast/SVC layer selection)</li>\n<li><strong>Not</strong> terminate E2EE; never access plaintext media</li>\n<li>Support <strong>DIDComm-based</strong> signaling for joins and control</li>\n</ul>\n<p><strong>sfu-mediator</strong> implementations MAY:</p>\n<ul>\n<li>Support recording to Encrypted Data Vaults (see Vaults 1.0)</li>\n<li>Provide bandwidth estimation and quality recommendations</li>\n</ul>\n<hr>\n<h2 id=\"Topology-Selection\">Topology Selection</h2>\n<p>Clients SHOULD select topology based on participant count and mediator capabilities:</p>\n<h3 id=\"Mesh-Topology-28-participants\">Mesh Topology (2–8 participants)</h3>\n<ul>\n<li>Each participant connects directly to every other participant</li>\n<li>N participants = N×(N-1)/2 peer connections</li>\n<li>Uses messages: <code>propose</code>, <code>offer</code>, <code>answer</code>, <code>ice</code>, <code>end</code></li>\n<li>Mediator role: <strong>Passive</strong> router (forwards DIDComm messages only)</li>\n<li>Works with any DIDComm mediator <strong>or with OOB links and direct delivery</strong></li>\n</ul>\n<h3 id=\"SFU-Topology-10100-participants\">SFU Topology (10–100+ participants)</h3>\n<ul>\n<li>Each participant connects to the mediator’s SFU</li>\n<li>N participants = N connections (all to SFU)</li>\n<li>Uses messages: <code>create-room</code>, <code>join-room</code>, <code>room-offer</code>, <code>room-answer</code>, <code>participant-joined</code>, etc.</li>\n<li>Mediator role: <strong>Active</strong> SFU (processes RTP, forwards media; never decrypts SFrame)</li>\n<li>Requires mediator with <code>sfu-mediator</code> role; <strong>invites may be OOB links</strong></li>\n</ul>\n<h3 id=\"Discovery\">Discovery</h3>\n<p>Clients MUST use Discover Features 2.0 to check SFU support:</p>\n<p><strong>Query</strong></p>\n<pre><code class=\"language-json\">{\n  \"type\": \"https://didcomm.org/discover-features/2.0/queries\",\n  \"body\": {\n    \"queries\": [\n      {\"feature-type\": \"protocol\", \"match\": \"https://didcomm.org/webrtc/1.*\"}\n    ]\n  }\n}\n</code></pre>\n<p><strong>Response indicating SFU support</strong></p>\n<pre><code class=\"language-json\">{\n  \"type\": \"https://didcomm.org/discover-features/2.0/disclose\",\n  \"body\": {\n    \"disclosures\": [{\n      \"feature-type\": \"protocol\",\n      \"id\": \"https://didcomm.org/webrtc/1.0\",\n      \"roles\": [\"sfu-mediator\"],\n      \"extensions\": {\n        \"max_participants\": 100,\n        \"simulcast\": true,\n        \"svc\": [\"VP9\"],\n        \"e2ee\": \"sframe\",\n        \"recording\": true\n      }\n    }]\n  }\n}\n</code></pre>\n<p>If mediator responds without <code>sfu-mediator</code> role, client SHOULD use mesh topology or reject calls with >8 participants.</p>\n<hr>\n<h2 id=\"States\">States</h2>\n<table>\n<thead>\n<tr>\n<th>State</th>\n<th>Event</th>\n<th>Next State</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>IDLE</code></td>\n<td><code>propose</code> sent</td>\n<td><code>PROPOSING</code></td>\n<td>Caller initiates connection request (mesh)</td>\n</tr>\n<tr>\n<td><code>IDLE</code></td>\n<td><code>create-room</code> sent</td>\n<td><code>CREATING_ROOM</code></td>\n<td>Host creates SFU room</td>\n</tr>\n<tr>\n<td><code>PROPOSING</code></td>\n<td><code>offer</code> sent</td>\n<td><code>OFFERING</code></td>\n<td>Caller sends SDP offer with fingerprint proof</td>\n</tr>\n<tr>\n<td><code>CREATING_ROOM</code></td>\n<td><code>room-created</code> received</td>\n<td><code>ROOM_ACTIVE</code></td>\n<td>SFU room ready, host can invite</td>\n</tr>\n<tr>\n<td><code>ROOM_ACTIVE</code></td>\n<td><code>join-room</code> sent</td>\n<td><code>JOINING</code></td>\n<td>Participant joining SFU room</td>\n</tr>\n<tr>\n<td><code>OFFERING</code></td>\n<td><code>answer</code> received</td>\n<td><code>CONNECTING</code></td>\n<td>SDP exchange complete, ICE gathering in progress</td>\n</tr>\n<tr>\n<td><code>JOINING</code></td>\n<td><code>room-offer</code> received</td>\n<td><code>CONNECTING</code></td>\n<td>SFU sends offer to participant</td>\n</tr>\n<tr>\n<td><code>CONNECTING</code></td>\n<td>ICE connected</td>\n<td><code>CONNECTED</code></td>\n<td>Media flowing, connection established</td>\n</tr>\n<tr>\n<td><code>CONNECTED</code></td>\n<td><code>renegotiate</code>/<code>update</code></td>\n<td><code>CONNECTED</code></td>\n<td>Track changes, ICE restart, or parameter updates</td>\n</tr>\n<tr>\n<td><code>CONNECTED</code></td>\n<td><code>end</code>/<code>leave-room</code></td>\n<td><code>CLOSED</code></td>\n<td>Session terminated</td>\n</tr>\n<tr>\n<td><code>*</code></td>\n<td><code>reject</code> received</td>\n<td><code>CLOSED</code></td>\n<td>Proposal or connection rejected</td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2 id=\"Out-of-Band-OOB-Join-Links\">Out-of-Band (OOB) Join Links</h2>\n<p>This section enables <strong>one-click joins</strong> (QR/URL) <strong>without</strong> a mediator for message delivery. After the click, all signaling proceeds over <strong>DIDComm v2 authcrypt</strong> to endpoints specified in the link.</p>\n<h3 id=\"Link-formats\">Link formats</h3>\n<p><strong>Custom scheme (preferred)</strong></p>\n<pre><code>didcomm://?_oob=&#x3C;base64url(webrtc-invitation)>\n</code></pre>\n<p><strong>HTTPS fallback (keep secret in fragment)</strong></p>\n<pre><code>https://join.example/webrtc#oob=&#x3C;base64url(webrtc-invitation)>\n</code></pre>\n<h3 id=\"Invitation-payload\">Invitation payload</h3>\n<p>The <code>_oob</code> value decodes to a compact JSON envelope (not sent over the wire directly after click):</p>\n<p><strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/invite-url</code></p>\n<pre><code class=\"language-json\">{\n  \"type\": \"https://didcomm.org/webrtc/1.0/invite-url\",\n  \"from\": \"did:example:alice#key-1\",\n  \"body\": {\n    \"topology\": \"mesh\",                           // or \"sfu\"\n    \"display\": \"Alice (Ajna)\",                    // optional inviter display\n    \"thid\": \"7f1b8c5e-...\",                       // thread to bind follow-ups\n    \"exp\": 1731200000,                            // short TTL (seconds)\n    \"one_time\": true,                             // single-use token\n    \"cap\": { \"media\": [\"audio\",\"video\"], \"data\": true },\n    \"policy\": \"relay-preferred\",\n    \"service\": [\n      { \"transport\": \"https\", \"endpoint\": \"https://agent.alice.example/didcomm\", \"return_route\": \"all\" },\n      { \"transport\": \"ws\", \"endpoint\": \"wss://agent.alice.example/didcomm\" }\n    ],\n    \"ice_servers\": [\n      { \"urls\": [\"stun:stun.verid.id:3478\"] },\n      { \"urls\": [\"turns:turn.verid.id:5349\"], \"username\": \"u\", \"credential\": \"p\" }\n    ],\n    \"sfu\": {\n      \"endpoint\": \"wss://sfu.verid.id/room-xyz\",\n      \"room_id\": \"room-xyz\",\n      \"features\": { \"simulcast\": true, \"e2ee\": true }\n    },\n    \"prekey\": { \"kty\": \"OKP\", \"crv\": \"X25519\", \"x\": \"&#x3C;base64url>\" }, // ephemeral ECDH prekey\n    \"constraints\": {\n      \"aud\": \"optional:did:example:bob\",\n      \"origin\": \"https://join.example\",\n      \"use_limit\": 1\n    },\n    \"proof\": {\n      \"alg\": \"EdDSA\",\n      \"kid\": \"did:example:alice#key-1\",\n      \"jws\": \"&#x3C;compact JWS over deterministic payload>\"\n    }\n  }\n}\n</code></pre>\n<p><strong>Receiver behavior</strong></p>\n<ol>\n<li>Decode <code>_oob</code>, verify <code>proof.jws</code> (issuer DID, <code>kid</code>), check <code>exp</code>, <code>one_time</code> / <code>use_limit</code>, <code>origin</code>, and (if present) <code>aud</code>.</li>\n<li>Start <strong>authcrypt</strong> DIDComm to one of the provided <code>service.endpoint</code>s using ECDH with the <strong>ephemeral <code>prekey</code></strong>.</li>\n<li>Send the first protocol message (<code>propose</code> for mesh or <code>join-room</code> for SFU) with header <code>oob_thid</code> (below).</li>\n</ol>\n<h3 id=\"OOB-acknowledgment-header\">OOB acknowledgment header</h3>\n<p>First on-wire message following an OOB link MUST include:</p>\n<pre><code>headers.oob_thid = &#x3C;the invitation thid>\n</code></pre>\n<p>This lets the inviter/SFU invalidate the token and enforce <code>use_limit</code>.</p>\n<h3 id=\"P2P-OOB-flow-mesh\">P2P OOB flow (mesh)</h3>\n<ol>\n<li>Alice generates <code>invite-url</code> (short TTL, one-time) with <code>service</code> endpoints + TURN/STUN + <code>prekey</code>.</li>\n<li>Bob clicks link → verifies → sends <code>propose</code> (with <code>oob_thid</code>) <strong>directly</strong> to Alice’s endpoint.</li>\n<li>Proceed with normal <code>offer</code>/<code>answer</code> + <code>ice</code>.</li>\n</ol>\n<h3 id=\"SFU-OOB-flow\">SFU OOB flow</h3>\n<ol>\n<li>Host creates room via <code>create-room</code>, then mints <code>invite-url</code> containing <code>sfu.endpoint</code>, <code>room_id</code>, <code>prekey</code>, TURN/STUN.</li>\n<li>Participant clicks link → verifies → sends <code>join-room</code> (with <code>oob_thid</code>) <strong>directly</strong> to SFU/host’s endpoint.</li>\n<li>Continue with <code>room-offer</code>/<code>room-answer</code> + <code>ice</code>.</li>\n</ol>\n<p><strong>Security notes</strong></p>\n<ul>\n<li>Use fragments (<code>#oob=</code>) for HTTPS to keep secrets out of server logs.</li>\n<li>Treat <code>thid</code> as the link token and mark spent on first successful use.</li>\n<li>Rotate TURN REST credentials frequently; avoid long-lived secrets in links.</li>\n</ul>\n<hr>\n<h2 id=\"Message-Reference\">Message Reference</h2>\n<p>All messages use DIDComm v2 with <code>type = https://didcomm.org/webrtc/1.0/&#x3C;message-name></code>, standard headers (<code>id</code>, <code>thid</code>, <code>from</code>, <code>to</code>, <code>expires_time</code>). Use <strong>authcrypt</strong>.</p>\n<blockquote>\n<p>When a session is bootstrapped from an OOB link, the first message MUST include <code>headers.oob_thid</code>.</p>\n</blockquote>\n<h4 id=\"invite-url\">invite-url</h4>\n<p><strong>Purpose</strong>: Embed in links/QRs only; bootstraps direct DIDComm contact.</p>\n<p><strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/invite-url</code>\n<strong>Fields</strong>: As specified in <strong>Out-of-Band Join Links</strong>.</p>\n<hr>\n<h3 id=\"Mesh-Topology-Messages\">Mesh Topology Messages</h3>\n<h4 id=\"propose\">propose</h4>\n<p>Caller initiates a WebRTC session request.</p>\n<p><strong>From</strong>: caller → callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"media\": [\"audio\",\"video\"],\n  \"data\": true,\n  \"topology\": \"mesh\",\n  \"trickle\": true,\n  \"policy\": \"relay-preferred\",\n  \"ice_servers\": [\n    {\"urls\": [\"stun:stun.l.example.net:3478\"]},\n    {\"urls\": [\"turns:turn.l.example.net:5349\"], \"username\": \"u\", \"credential\": \"p\"}\n  ]\n}\n</code></pre>\n<p><strong>Headers (if OOB)</strong>: <code>{ \"oob_thid\": \"&#x3C;thid-from-invite>\" }</code></p>\n<hr>\n<h4 id=\"offer\">offer</h4>\n<p><strong>From</strong>: caller → callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"sdp\": \"&#x3C;SDP offer string>\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 12:34:...:ab\"],\n    \"dtls_role\": \"actpass\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:alice#key-1\",\n    \"created\": 1731140000,\n    \"expires\": 1731140300,\n    \"jws\": \"&#x3C;JWS signature>\"\n  },\n  \"trickle\": true,\n  \"policy\": \"relay-preferred\",\n  \"ice_servers\": [...],\n  \"tie_breaker\": 823746120\n}\n</code></pre>\n<hr>\n<h4 id=\"answer\">answer</h4>\n<p><strong>From</strong>: callee → caller\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"sdp\": \"&#x3C;SDP answer string>\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 98:76:...:cd\"],\n    \"dtls_role\": \"passive\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:bob#key-1\",\n    \"created\": 1731140005,\n    \"expires\": 1731140305,\n    \"jws\": \"&#x3C;JWS signature>\"\n  }\n}\n</code></pre>\n<hr>\n<h4 id=\"ice\">ice</h4>\n<p>Exchange trickle ICE candidates.</p>\n<p><strong>From</strong>: caller ↔ callee OR participant ↔ sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"candidate\": \"candidate:0 1 UDP 2122252543 192.168.1.5 60769 typ host ...\",\n  \"sdpMid\": \"0\",\n  \"sdpMLineIndex\": 0,\n  \"endOfCandidates\": false\n}\n</code></pre>\n<hr>\n<h4 id=\"renegotiate\">renegotiate</h4>\n<p><strong>From</strong>: caller ↔ callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"reason\": \"add-screenshare\",\n  \"ice_restart\": false\n}\n</code></pre>\n<hr>\n<h4 id=\"update\">update</h4>\n<p><strong>From</strong>: caller ↔ callee OR participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"action\": \"mute\",\n  \"media_type\": \"audio\",\n  \"value\": true\n}\n</code></pre>\n<hr>\n<h4 id=\"reject\">reject</h4>\n<p><strong>From</strong>: callee → caller\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"reason\": \"incompatible-capabilities\"\n}\n</code></pre>\n<hr>\n<h4 id=\"end\">end</h4>\n<p><strong>From</strong>: caller ↔ callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"reason\": \"hangup\",\n  \"scope\": \"peer\"\n}\n</code></pre>\n<hr>\n<h3 id=\"SFU-Topology-Messages\">SFU Topology Messages</h3>\n<h4 id=\"create-room\">create-room</h4>\n<p><strong>From</strong>: host → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"max_participants\": 100,\n  \"features\": { \"simulcast\": true, \"e2ee\": true, \"recording\": false },\n  \"policy\": { \"ice_policy\": \"relay-preferred\", \"codec_preferences\": [\"VP9\",\"VP8\",\"H.264\"] }\n}\n</code></pre>\n<h4 id=\"room-created\">room-created</h4>\n<p><strong>From</strong>: sfu-mediator → host\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sfu_endpoint\": \"wss://mediator.example/sfu/room-xyz\",\n  \"ice_servers\": [\n    {\"urls\": [\"turns:mediator.example:5349\"], \"username\": \"...\", \"credential\": \"...\"}\n  ],\n  \"e2ee_config\": { \"suite\": \"sframe-aes-gcm-256\", \"key_distribution\": \"didcomm\" }\n}\n</code></pre>\n<h4 id=\"invite\">invite</h4>\n<p><strong>From</strong>: host → participant (via mediator routing or <strong>as OOB link</strong>)\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"media\": [\"audio\",\"video\"],\n  \"sfu_endpoint\": \"wss://mediator.example/sfu/room-xyz\"\n}\n</code></pre>\n<h4 id=\"join-room\">join-room</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Headers (if OOB)</strong>: <code>{ \"oob_thid\": \"&#x3C;thid-from-invite>\" }</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"media\": [\"audio\",\"video\"],\n  \"capabilities\": { \"simulcast\": true, \"max_recv_bitrate\": 5000000, \"codecs\": [\"VP9\",\"VP8\"] }\n}\n</code></pre>\n<h4 id=\"room-offer\">room-offer</h4>\n<p><strong>From</strong>: sfu-mediator → participant\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sdp\": \"&#x3C;SDP with receive-only tracks>\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 ...\"],\n    \"dtls_role\": \"actpass\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:sfu#key-1\",\n    \"created\": 1731140010,\n    \"expires\": 1731140310,\n    \"jws\": \"...\"\n  },\n  \"participants\": [\n    {\n      \"did\": \"did:example:alice\",\n      \"stream_id\": \"stream-alice-1\",\n      \"media\": [\"audio\",\"video\"],\n      \"e2ee_key_id\": \"kid-alice-123\"\n    }\n  ]\n}\n</code></pre>\n<h4 id=\"room-answer\">room-answer</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sdp\": \"&#x3C;SDP answer with send-only tracks>\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 ...\"],\n    \"dtls_role\": \"passive\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:bob#key-1\",\n    \"created\": 1731140011,\n    \"expires\": 1731140311,\n    \"jws\": \"...\"\n  }\n}\n</code></pre>\n<h4 id=\"participant-joined\">participant-joined</h4>\n<p><strong>From</strong>: sfu-mediator → all participants\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"participant\": { \"did\": \"did:example:bob\", \"stream_id\": \"stream-bob-1\", \"media\": [\"audio\",\"video\"] }\n}\n</code></pre>\n<h4 id=\"leave-room\">leave-room</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"reason\": \"user-left\"\n}\n</code></pre>\n<h4 id=\"participant-left\">participant-left</h4>\n<p><strong>From</strong>: sfu-mediator → all participants\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"participant\": \"did:example:bob\",\n  \"reason\": \"user-left\"\n}\n</code></pre>\n<h4 id=\"quality-update\">quality-update</h4>\n<p><strong>From</strong>: sfu-mediator → participant\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"updates\": [{ \"stream_id\": \"stream-alice-1\", \"spatial_layer\": \"medium\", \"temporal_layer\": 2, \"max_bitrate\": 1500000 }]\n}\n</code></pre>\n<h4 id=\"simulcast-control\">simulcast-control</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"preferences\": { \"stream-alice-1\": \"high\", \"stream-charlie-1\": \"low\" }\n}\n</code></pre>\n<h4 id=\"destroy-room\">destroy-room</h4>\n<p><strong>From</strong>: host → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"reason\": \"host-ended\"\n}\n</code></pre>\n<h4 id=\"ping--pong\">ping / pong</h4>\n<p><strong>From</strong>: any ↔ any\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{ \"timestamp\": 1698765432000 }\n</code></pre>\n<hr>\n<h2 id=\"End-to-End-Encryption-SFU-Mode\">End-to-End Encryption (SFU Mode)</h2>\n<h3 id=\"Model\">Model</h3>\n<p>In SFU topology, media traverses the mediator. E2EE is achieved with <strong>SFrame</strong>, which encrypts encoded frames at the application layer using a <strong>symmetric “sender key” per sender</strong>.</p>\n<p><strong>Key properties</strong></p>\n<ul>\n<li>Each sender maintains a symmetric SFrame key identified by <code>kid</code>.</li>\n<li>Frames are encrypted before leaving the client and decrypted after reception.</li>\n<li>The SFU forwards opaque ciphertext; it never sees plaintext.</li>\n</ul>\n<h3 id=\"Key-Distribution-strictly-end-to-end\">Key Distribution (strictly end-to-end)</h3>\n<ol>\n<li>\n<p><strong>Sender creates</strong> a new random symmetric SFrame key (<code>kid</code>), e.g., on join or rotation.</p>\n</li>\n<li>\n<p><strong>Sender wraps</strong> that key <strong>for each recipient</strong> using recipients’ DID public keys (HPKE/ECDH-ES → AEAD).</p>\n</li>\n<li>\n<p><strong>Sender distributes</strong> wrapped keys via DIDComm:</p>\n<ul>\n<li><code>key-announce</code> (new sender key)</li>\n<li><code>key-rotate</code> (periodic or on membership change)</li>\n<li><code>key-revoke</code> (e.g., when removing a participant)</li>\n</ul>\n</li>\n<li>\n<p><strong>Recipients unwrap</strong> and install the key mapped to <code>sender DID + kid</code>.</p>\n</li>\n</ol>\n<blockquote>\n<p><strong>The SFU MUST NOT generate, unwrap, or rewrap sender keys.</strong> It may relay the DIDComm key messages but never handle key plaintext.</p>\n</blockquote>\n<h3 id=\"Key-management-messages\">Key management messages</h3>\n<h4 id=\"key-announce\">key-announce</h4>\n<p><strong>From</strong>: sender → room (via SFU routing is OK; remains E2E)\n<strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/key-announce</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sender\": \"did:example:alice\",\n  \"suite\": \"sframe-aes-gcm-256\",\n  \"kid\": \"kid-alice-123\",\n  \"wrapped_keys\": [\n    { \"recipient\": \"did:example:bob#key-1\", \"jwe\": \"&#x3C;HPKE/JWE>\" },\n    { \"recipient\": \"did:example:charlie#key-1\", \"jwe\": \"&#x3C;HPKE/JWE>\" }\n  ],\n  \"created\": 1731140020\n}\n</code></pre>\n<h4 id=\"key-rotate\">key-rotate</h4>\n<p><strong>From</strong>: sender → room\n<strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/key-rotate</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sender\": \"did:example:alice\",\n  \"old_kid\": \"kid-alice-123\",\n  \"new_kid\": \"kid-alice-124\",\n  \"wrapped_keys\": [ /* as above for new_kid */ ],\n  \"reason\": \"membership-change\"\n}\n</code></pre>\n<h4 id=\"key-revoke\">key-revoke</h4>\n<p><strong>From</strong>: host/sender → room\n<strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/key-revoke</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sender\": \"did:example:alice\",\n  \"kid\": \"kid-alice-123\",\n  \"reason\": \"participant-removed\"\n}\n</code></pre>\n<h3 id=\"Insertable-Streams-illustrative\">Insertable Streams (illustrative)</h3>\n<pre><code class=\"language-javascript\">// Encrypt outgoing frames with current sender key\nconst tx = new TransformStream({\n  transform: (encoded, ctl) => ctl.enqueue(sframeEncrypt(encoded, senderKey))\n});\nsender.transform = tx;\n\n// Decrypt incoming frames using sender DID + kid\nconst rx = new TransformStream({\n  transform: (encoded, ctl) => {\n    const key = keyStore.get(encoded.meta.senderDid, encoded.meta.kid);\n    ctl.enqueue(sframeDecrypt(encoded, key));\n  }\n});\nreceiver.transform = rx;\n</code></pre>\n<hr>\n<h2 id=\"Flows\">Flows</h2>\n<h3 id=\"Basic-Two-Party-Call-Mesh\">Basic Two-Party Call (Mesh)</h3>\n<ol>\n<li><strong>Alice → Bob</strong>: <code>propose</code> (media types, ICE policy)</li>\n<li><strong>Alice</strong>: Create <code>RTCPeerConnection</code>, generate SDP offer, sign fingerprint proof</li>\n<li><strong>Alice → Bob</strong>: <code>offer</code> (SDP + <code>fingerprint_proof</code>)</li>\n<li><strong>Bob</strong>: Verify proof (DID doc); set remote; create answer; sign fingerprint</li>\n<li><strong>Bob → Alice</strong>: <code>answer</code> (SDP + <code>fingerprint_proof</code>)</li>\n<li><strong>Both</strong>: Exchange <code>ice</code> (trickle); DTLS handshake; media flows P2P</li>\n<li><strong>Either</strong>: <code>end</code> to terminate</li>\n</ol>\n<h3 id=\"Multi-Party-Call-with-SFU-50-participants\">Multi-Party Call with SFU (50 participants)</h3>\n<ol>\n<li><strong>Host → SFU</strong>: <code>create-room</code> (E2EE enabled)</li>\n<li><strong>SFU → Host</strong>: <code>room-created</code> (room_id, sfu_endpoint)</li>\n<li><strong>Host → invitees</strong>: <code>invite</code> <strong>or OOB links</strong></li>\n<li><strong>Participant → SFU</strong>: <code>join-room</code> (capabilities)</li>\n<li><strong>SFU → Participant</strong>: <code>room-offer</code></li>\n<li><strong>Participant → SFU</strong>: <code>room-answer</code></li>\n<li><strong>Sender(s)</strong>: <code>key-announce</code> (SFrame sender keys) via DIDComm</li>\n<li><strong>Everyone</strong>: trickle <code>ice</code>; media flows via SFU (still E2EE)</li>\n</ol>\n<h3 id=\"OOB-Link-P2P\">OOB Link (P2P)</h3>\n<ol>\n<li><strong>Alice</strong> creates <code>invite-url</code> (short TTL, one-time, <code>prekey</code>).</li>\n<li><strong>Bob</strong> clicks, verifies, and sends <code>propose</code> <strong>directly</strong> to Alice (<code>oob_thid</code> set).</li>\n<li>Continue with <code>offer</code>/<code>answer</code>/<code>ice</code>.</li>\n</ol>\n<h3 id=\"OOB-Link-SFU\">OOB Link (SFU)</h3>\n<ol>\n<li><strong>Host</strong> creates room, mints <code>invite-url</code> (includes <code>sfu.endpoint</code>, <code>room_id</code>, <code>prekey</code>).</li>\n<li><strong>Participant</strong> clicks, verifies, and sends <code>join-room</code> to SFU (<code>oob_thid</code> set).</li>\n<li>Continue with <code>room-offer</code>/<code>room-answer</code>/<code>ice</code>, then E2EE key announcements.</li>\n</ol>\n<h3 id=\"Glare-Resolution-Mesh\">Glare Resolution (Mesh)</h3>\n<p>If both sides send <code>offer</code> simultaneously:</p>\n<ol>\n<li>Compare <code>tie_breaker</code>; the lower value <strong>retracts</strong> and answers the other.</li>\n<li>DTLS <code>setup</code> roles SHOULD follow JSEP perfect-negotiation norms.</li>\n</ol>\n<h3 id=\"Dynamic-Quality-Adaptation-SFU\">Dynamic Quality Adaptation (SFU)</h3>\n<ol>\n<li>SFU detects congestion.</li>\n<li><strong>SFU → Participant</strong>: <code>quality-update</code> (layer caps).</li>\n<li>Client adjusts simulcast/SVC subscriptions and encodings.</li>\n</ol>\n<h3 id=\"Using-with-Message-Pickup\">Using with Message Pickup</h3>\n<p>When callee is offline:</p>\n<ol>\n<li>Caller’s <code>propose</code>/<code>offer</code> queued at mediator (mesh), or <code>invite</code> queued.</li>\n<li>Callee polls via Message Pickup 4.0.</li>\n<li>Callee processes and continues as usual.</li>\n</ol>\n<hr>\n<h2 id=\"Security-Considerations\">Security Considerations</h2>\n<h3 id=\"Authentication--Integrity\">Authentication &#x26; Integrity</h3>\n<ul>\n<li>\n<p><strong>DIDComm authcrypt</strong> for all signaling messages.</p>\n</li>\n<li>\n<p><strong>Fingerprint binding</strong>: <code>fingerprint_proof</code> JWS binds WebRTC DTLS certificate(s) to a DID key.</p>\n<ul>\n<li>Use <code>alg: \"EdDSA\"</code> with <code>kid</code> referencing a DID URL.</li>\n<li>Sign a canonical payload containing: <code>thid</code>, <strong>all</strong> <code>a=fingerprint</code> values, <code>dtls_role</code>, <code>created</code>, <code>expires</code>.</li>\n<li>Verifier checks freshness, DID key validity, and exact SDP fingerprint match.</li>\n</ul>\n</li>\n</ul>\n<h3 id=\"OOB-Link-Security\">OOB Link Security</h3>\n<ul>\n<li><strong>Short TTLs</strong> and <strong>single-use</strong> (<code>one_time</code> or <code>use_limit</code>) to mitigate link leaks.</li>\n<li><strong>Audience binding</strong> (<code>constraints.aud</code>) if the invitee DID is known.</li>\n<li><strong>Origin binding</strong> (<code>constraints.origin</code>) for web joins; keep <code>_oob</code> in the URL <strong>fragment</strong>.</li>\n<li><strong>Ephemeral prekeys</strong> per link; never reuse; do not expose long-term keys.</li>\n</ul>\n<h3 id=\"Privacy\">Privacy</h3>\n<ul>\n<li>Pairwise DIDs to prevent correlation across sessions.</li>\n<li><code>relay-only</code> ICE policy to hide IPs (at higher cost/latency).</li>\n<li>Prefer mDNS candidates over private IP exposure.</li>\n<li>Minimize metadata in headers and <code>reason</code> fields.</li>\n</ul>\n<h3 id=\"Media-Encryption\">Media Encryption</h3>\n<ul>\n<li>Mesh: DTLS-SRTP provides E2EE by default.</li>\n<li>SFU: <strong>SFrame</strong> provides application-layer E2EE; keys are <strong>symmetric</strong> and distributed <strong>end-to-end</strong> by clients.</li>\n</ul>\n<h3 id=\"Problem-Reports-Report-Problem-20-codes\">Problem Reports (Report Problem 2.0 codes)</h3>\n<ul>\n<li><code>incompatible-capabilities</code>, <code>ice-gathering-timeout</code>, <code>turn-auth-failed</code>,\n<code>unverified-fingerprint</code>, <code>glare-timeout</code>, <code>policy-required</code>,\n<code>topology-unsupported</code>, <code>room-full</code>, <code>sfu-unavailable</code>.</li>\n</ul>\n<hr>\n<h2 id=\"Interop-Notes\">Interop Notes</h2>\n<h3 id=\"DID-Document-Service-Endpoints-Optional\">DID Document Service Endpoints (Optional)</h3>\n<p>Agents MAY publish WebRTC ICE server configuration:</p>\n<pre><code class=\"language-json\">{\n  \"id\": \"#webrtc-ice\",\n  \"type\": \"webrtc-ice-servers\",\n  \"serviceEndpoint\": \"https://agent.alice.example/ice\",\n  \"accept\": [\"webrtc/ice-servers/1.0\"]\n}\n</code></pre>\n<p>SFU mediators MAY publish SFU capabilities:</p>\n<pre><code class=\"language-json\">{\n  \"id\": \"#webrtc-sfu\",\n  \"type\": \"webrtc-sfu\",\n  \"serviceEndpoint\": \"wss://mediator.example/sfu\",\n  \"capabilities\": {\n    \"max_participants\": 100,\n    \"simulcast\": true,\n    \"codecs\": [\"VP9\",\"VP8\",\"H.264\"],\n    \"e2ee\": [\"sframe\"]\n  }\n}\n</code></pre>\n<h3 id=\"Composition-with-Other-Protocols\">Composition with Other Protocols</h3>\n<ul>\n<li><strong>Discover Features 2.0</strong>: Capability negotiation</li>\n<li><strong>Message Pickup 4.0</strong>: Async signaling when offline</li>\n<li><strong>Coordinate Mediation 3.0</strong>: DID registration with mediators</li>\n<li><strong>Report Problem 2.0</strong>: Error reporting</li>\n<li><strong>Vaults 1.0</strong>: Recording storage (encrypted)</li>\n</ul>\n<h3 id=\"Platform-Considerations\">Platform Considerations</h3>\n<ul>\n<li><strong>Browser</strong>: <code>RTCPeerConnection</code> + Insertable Streams for SFrame</li>\n<li><strong>Mobile</strong>: Native WebRTC libs (iOS WebRTC.framework, Android libwebrtc)</li>\n<li><strong>Rust</strong>: <code>webrtc-rs</code> (or equivalents)</li>\n<li><strong>Node.js</strong>: <code>wrtc</code> / <code>node-webrtc</code></li>\n</ul>\n<hr>\n<h2 id=\"Implementation-Hints\">Implementation Hints</h2>\n<h3 id=\"Fingerprint-Extraction--Proof\">Fingerprint Extraction &#x26; Proof</h3>\n<pre><code class=\"language-javascript\">function extractFingerprints(sdp) {\n  return [...sdp.matchAll(/^a=fingerprint:(\\S+)\\s+(.+)$/gm)]\n         .map(m => `${m[1]} ${m[2]}`);\n}\n\nfunction createFingerprintProof({ thid, fingerprints, dtls_role, didKey }) {\n  const payload = { thid, fingerprints, dtls_role, created: Date.now(), expires: Date.now()+300000 };\n  return signJWS(payload, didKey, { alg: \"EdDSA\", kid: didKey.kid });\n}\n</code></pre>\n<h3 id=\"Trickle-ICE\">Trickle ICE</h3>\n<pre><code class=\"language-javascript\">pc.onicecandidate = (e) => {\n  sendDIDComm({\n    type: \"https://didcomm.org/webrtc/1.0/ice\",\n    thid: sessionId,\n    body: e.candidate ? {\n      candidate: e.candidate.candidate,\n      sdpMid: e.candidate.sdpMid,\n      sdpMLineIndex: e.candidate.sdpMLineIndex,\n      endOfCandidates: false\n    } : { endOfCandidates: true }\n  });\n};\n</code></pre>\n<h3 id=\"OOB-Link-Validation-Receiver\">OOB Link Validation (Receiver)</h3>\n<pre><code class=\"language-javascript\">async function handleOOB(oob) {\n  const { body } = oob;\n  assert(Date.now()/1000 &#x3C; body.exp);\n  await verifyJWS(body.proof.jws, body.proof.kid); // EdDSA over canonical body\n  // Start DIDComm to body.service[0].endpoint using ECDH with body.prekey\n}\n</code></pre>\n<h3 id=\"SFU-Implementation-Hints\">SFU Implementation Hints</h3>\n<ul>\n<li>ICE-Lite recommended; forward RTP with header extensions preserved for SFrame.</li>\n<li>Respect client <code>simulcast-control</code> requests and issue <code>quality-update</code> as needed.</li>\n<li>Never unwrap or generate SFrame keys; only relay key messages.</li>\n</ul>\n<hr>\n<h2 id=\"References\">References</h2>\n<ul>\n<li>WebRTC 1.0: Real-Time Communication Between Browsers — <a href=\"https://www.w3.org/TR/webrtc/\">https://www.w3.org/TR/webrtc/</a></li>\n<li>RFC 8829 — JavaScript Session Establishment Protocol (JSEP)</li>\n<li>RFC 8445 — Interactive Connectivity Establishment (ICE)</li>\n<li>SFrame — IETF draft: draft-ietf-sframe-enc</li>\n<li>Insertable Streams for MediaStreamTrack — <a href=\"https://w3c.github.io/webrtc-encoded-transform/\">https://w3c.github.io/webrtc-encoded-transform/</a></li>\n<li>DIDComm Messaging Specification v2.0 — <a href=\"https://identity.foundation/didcomm-messaging/spec/v2.0/\">https://identity.foundation/didcomm-messaging/spec/v2.0/</a></li>\n<li>Discover Features Protocol 2.0 — <a href=\"https://didcomm.org/discover-features/2.0/\">https://didcomm.org/discover-features/2.0/</a></li>\n<li>Message Pickup Protocol 4.0 — <a href=\"https://didcomm.org/messagepickup/4.0/\">https://didcomm.org/messagepickup/4.0/</a></li>\n<li>Coordinate Mediation Protocol 3.0 — <a href=\"https://didcomm.org/coordinate-mediation/3.0/\">https://didcomm.org/coordinate-mediation/3.0/</a></li>\n<li>Vaults Protocol 1.0 — <a href=\"https://didcomm.org/vaults/1.0/\">https://didcomm.org/vaults/1.0/</a></li>\n</ul>\n<hr>","frontmatter":{"title":"WebRTC","tags":["webrtc","video","audio","p2p","sfu","signaling","ice","stun","turn","dtls","srtp","media-sharing","real-time","multiparty","scalable","e2ee","sframe","oob","links","qr"],"license":"MIT","publisher":"vinaysingh8866","status":"Proposed","piuri":"https://didcomm.org/webrtc/1.0","summary":"A DIDComm v2 protocol for WebRTC video and audio communication supporting both peer-to-peer mesh (2–8 participants) and SFU-mediated topology (10–100+ participants), with DID-bound certificate fingerprints, E2EE via SFrame, privacy-preserving features, and out-of-band (OOB) one-click invite links.","authors":[{"name":"Vinay Singh","email":"vinay@verid.id"}]},"fields":{"modifiedDate":"Wed Jul 22 2026 15:46:37 GMT+0000 (Coordinated Universal Time)","avatar":"https://avatars.githubusercontent.com/u/12121665?v=4&s=48","version":"1.0"}}},"pageContext":{"matchPath":"webrtc/1.0/*","id":"e9c0aa7a-3c11-5ddc-ba5c-e20a99e1df00","html":"<h2 id=\"Summary\">Summary</h2>\n<p>The WebRTC 1.0 protocol enables secure video and audio communication using <strong>DIDComm v2</strong> as the signaling channel. It supports two topologies: <strong>peer-to-peer mesh</strong> for small calls (2–8 participants) and <strong>SFU-mediated</strong> for scalable calls (10–100+ participants). The protocol provides NAT traversal, <strong>binds WebRTC DTLS certificates to DID keys</strong> for MITM protection, supports <strong>end-to-end encryption (E2EE) with SFrame</strong> in SFU mode, and works seamlessly with DIDComm mediators and <strong>Message Pickup</strong> for offline scenarios.\nThis draft additionally defines <strong>Out-of-Band (OOB) Join Links</strong> so users can connect by clicking a link (or scanning a QR) delivered by any channel (email/SMS/DM) <strong>without requiring a mediator for the first hop</strong>—while keeping all subsequent signaling DID-authenticated and encrypted.</p>\n<hr />\n<h2 id=\"Goals\">Goals</h2>\n<ul>\n<li>Enable both P2P mesh and SFU-mediated topologies in one protocol</li>\n<li>Support ~2–8 participants via mesh, ~10–100+ via SFU</li>\n<li>Use DIDComm v2 <strong>authcrypt</strong> for signaling</li>\n<li>Bind identity to media streams via DID-signed certificate fingerprints</li>\n<li>Maintain E2EE in SFU mode using <strong>SFrame</strong> (Insertable Streams)</li>\n<li>Support privacy controls (relay-only mode, pairwise DIDs)</li>\n<li>Handle glare resolution, renegotiation, and ICE restart</li>\n<li>Work with standard DIDComm mediators (mesh) or SFU-capable mediators (SFU)</li>\n<li><strong>Provide link-native, mediator-free onboarding</strong> via OOB Join Links (QR/URL)</li>\n</ul>\n<hr />\n<h2 id=\"Roles\">Roles</h2>\n<ul>\n<li><strong>caller</strong>: Initiates peer-to-peer WebRTC sessions</li>\n<li><strong>callee</strong>: Receives peer-to-peer session proposals</li>\n<li><strong>host</strong>: Creates multi-party rooms and invites participants</li>\n<li><strong>participant</strong>: Joins multi-party sessions</li>\n<li><strong>sfu-mediator</strong>: DIDComm mediator with SFU media forwarding capabilities</li>\n</ul>\n<h3 id=\"Role-Requirements\">Role Requirements</h3>\n<p><strong>Standard DIDComm mediators</strong> support mesh topology with no additional requirements beyond Message Pickup and Coordinate Mediation.</p>\n<p><strong>sfu-mediator</strong> implementations MUST:</p>\n<ul>\n<li>Support WebRTC media forwarding (receive from all, forward to all)</li>\n<li>Manage room lifecycle (create, join, leave, destroy)</li>\n<li>Forward <strong>SFrame-encrypted</strong> frames without decrypting</li>\n<li>Provide TURN infrastructure for NAT traversal</li>\n<li>Implement quality adaptation (simulcast/SVC layer selection)</li>\n<li><strong>Not</strong> terminate E2EE; never access plaintext media</li>\n<li>Support <strong>DIDComm-based</strong> signaling for joins and control</li>\n</ul>\n<p><strong>sfu-mediator</strong> implementations MAY:</p>\n<ul>\n<li>Support recording to Encrypted Data Vaults (see Vaults 1.0)</li>\n<li>Provide bandwidth estimation and quality recommendations</li>\n</ul>\n<hr />\n<h2 id=\"Topology-Selection\">Topology Selection</h2>\n<p>Clients SHOULD select topology based on participant count and mediator capabilities:</p>\n<h3 id=\"Mesh-Topology-28-participants\">Mesh Topology (2–8 participants)</h3>\n<ul>\n<li>Each participant connects directly to every other participant</li>\n<li>N participants = N×(N-1)/2 peer connections</li>\n<li>Uses messages: <code>propose</code>, <code>offer</code>, <code>answer</code>, <code>ice</code>, <code>end</code></li>\n<li>Mediator role: <strong>Passive</strong> router (forwards DIDComm messages only)</li>\n<li>Works with any DIDComm mediator <strong>or with OOB links and direct delivery</strong></li>\n</ul>\n<h3 id=\"SFU-Topology-10100-participants\">SFU Topology (10–100+ participants)</h3>\n<ul>\n<li>Each participant connects to the mediator’s SFU</li>\n<li>N participants = N connections (all to SFU)</li>\n<li>Uses messages: <code>create-room</code>, <code>join-room</code>, <code>room-offer</code>, <code>room-answer</code>, <code>participant-joined</code>, etc.</li>\n<li>Mediator role: <strong>Active</strong> SFU (processes RTP, forwards media; never decrypts SFrame)</li>\n<li>Requires mediator with <code>sfu-mediator</code> role; <strong>invites may be OOB links</strong></li>\n</ul>\n<h3 id=\"Discovery\">Discovery</h3>\n<p>Clients MUST use Discover Features 2.0 to check SFU support:</p>\n<p><strong>Query</strong></p>\n<pre><code class=\"language-json\">{\n  \"type\": \"https://didcomm.org/discover-features/2.0/queries\",\n  \"body\": {\n    \"queries\": [\n      {\"feature-type\": \"protocol\", \"match\": \"https://didcomm.org/webrtc/1.*\"}\n    ]\n  }\n}\n</code></pre>\n<p><strong>Response indicating SFU support</strong></p>\n<pre><code class=\"language-json\">{\n  \"type\": \"https://didcomm.org/discover-features/2.0/disclose\",\n  \"body\": {\n    \"disclosures\": [{\n      \"feature-type\": \"protocol\",\n      \"id\": \"https://didcomm.org/webrtc/1.0\",\n      \"roles\": [\"sfu-mediator\"],\n      \"extensions\": {\n        \"max_participants\": 100,\n        \"simulcast\": true,\n        \"svc\": [\"VP9\"],\n        \"e2ee\": \"sframe\",\n        \"recording\": true\n      }\n    }]\n  }\n}\n</code></pre>\n<p>If mediator responds without <code>sfu-mediator</code> role, client SHOULD use mesh topology or reject calls with &gt;8 participants.</p>\n<hr />\n<h2 id=\"States\">States</h2>\n<table>\n<thead>\n<tr>\n<th>State</th>\n<th>Event</th>\n<th>Next State</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>IDLE</code></td>\n<td><code>propose</code> sent</td>\n<td><code>PROPOSING</code></td>\n<td>Caller initiates connection request (mesh)</td>\n</tr>\n<tr>\n<td><code>IDLE</code></td>\n<td><code>create-room</code> sent</td>\n<td><code>CREATING_ROOM</code></td>\n<td>Host creates SFU room</td>\n</tr>\n<tr>\n<td><code>PROPOSING</code></td>\n<td><code>offer</code> sent</td>\n<td><code>OFFERING</code></td>\n<td>Caller sends SDP offer with fingerprint proof</td>\n</tr>\n<tr>\n<td><code>CREATING_ROOM</code></td>\n<td><code>room-created</code> received</td>\n<td><code>ROOM_ACTIVE</code></td>\n<td>SFU room ready, host can invite</td>\n</tr>\n<tr>\n<td><code>ROOM_ACTIVE</code></td>\n<td><code>join-room</code> sent</td>\n<td><code>JOINING</code></td>\n<td>Participant joining SFU room</td>\n</tr>\n<tr>\n<td><code>OFFERING</code></td>\n<td><code>answer</code> received</td>\n<td><code>CONNECTING</code></td>\n<td>SDP exchange complete, ICE gathering in progress</td>\n</tr>\n<tr>\n<td><code>JOINING</code></td>\n<td><code>room-offer</code> received</td>\n<td><code>CONNECTING</code></td>\n<td>SFU sends offer to participant</td>\n</tr>\n<tr>\n<td><code>CONNECTING</code></td>\n<td>ICE connected</td>\n<td><code>CONNECTED</code></td>\n<td>Media flowing, connection established</td>\n</tr>\n<tr>\n<td><code>CONNECTED</code></td>\n<td><code>renegotiate</code>/<code>update</code></td>\n<td><code>CONNECTED</code></td>\n<td>Track changes, ICE restart, or parameter updates</td>\n</tr>\n<tr>\n<td><code>CONNECTED</code></td>\n<td><code>end</code>/<code>leave-room</code></td>\n<td><code>CLOSED</code></td>\n<td>Session terminated</td>\n</tr>\n<tr>\n<td><code>*</code></td>\n<td><code>reject</code> received</td>\n<td><code>CLOSED</code></td>\n<td>Proposal or connection rejected</td>\n</tr>\n</tbody>\n</table>\n<hr />\n<h2 id=\"Out-of-Band-OOB-Join-Links\">Out-of-Band (OOB) Join Links</h2>\n<p>This section enables <strong>one-click joins</strong> (QR/URL) <strong>without</strong> a mediator for message delivery. After the click, all signaling proceeds over <strong>DIDComm v2 authcrypt</strong> to endpoints specified in the link.</p>\n<h3 id=\"Link-formats\">Link formats</h3>\n<p><strong>Custom scheme (preferred)</strong></p>\n<pre><code>didcomm://?_oob=&lt;base64url(webrtc-invitation)&gt;\n</code></pre>\n<p><strong>HTTPS fallback (keep secret in fragment)</strong></p>\n<pre><code>https://join.example/webrtc#oob=&lt;base64url(webrtc-invitation)&gt;\n</code></pre>\n<h3 id=\"Invitation-payload\">Invitation payload</h3>\n<p>The <code>_oob</code> value decodes to a compact JSON envelope (not sent over the wire directly after click):</p>\n<p><strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/invite-url</code></p>\n<pre><code class=\"language-json\">{\n  \"type\": \"https://didcomm.org/webrtc/1.0/invite-url\",\n  \"from\": \"did:example:alice#key-1\",\n  \"body\": {\n    \"topology\": \"mesh\",                           // or \"sfu\"\n    \"display\": \"Alice (Ajna)\",                    // optional inviter display\n    \"thid\": \"7f1b8c5e-...\",                       // thread to bind follow-ups\n    \"exp\": 1731200000,                            // short TTL (seconds)\n    \"one_time\": true,                             // single-use token\n    \"cap\": { \"media\": [\"audio\",\"video\"], \"data\": true },\n    \"policy\": \"relay-preferred\",\n    \"service\": [\n      { \"transport\": \"https\", \"endpoint\": \"https://agent.alice.example/didcomm\", \"return_route\": \"all\" },\n      { \"transport\": \"ws\", \"endpoint\": \"wss://agent.alice.example/didcomm\" }\n    ],\n    \"ice_servers\": [\n      { \"urls\": [\"stun:stun.verid.id:3478\"] },\n      { \"urls\": [\"turns:turn.verid.id:5349\"], \"username\": \"u\", \"credential\": \"p\" }\n    ],\n    \"sfu\": {\n      \"endpoint\": \"wss://sfu.verid.id/room-xyz\",\n      \"room_id\": \"room-xyz\",\n      \"features\": { \"simulcast\": true, \"e2ee\": true }\n    },\n    \"prekey\": { \"kty\": \"OKP\", \"crv\": \"X25519\", \"x\": \"&lt;base64url&gt;\" }, // ephemeral ECDH prekey\n    \"constraints\": {\n      \"aud\": \"optional:did:example:bob\",\n      \"origin\": \"https://join.example\",\n      \"use_limit\": 1\n    },\n    \"proof\": {\n      \"alg\": \"EdDSA\",\n      \"kid\": \"did:example:alice#key-1\",\n      \"jws\": \"&lt;compact JWS over deterministic payload&gt;\"\n    }\n  }\n}\n</code></pre>\n<p><strong>Receiver behavior</strong></p>\n<ol>\n<li>Decode <code>_oob</code>, verify <code>proof.jws</code> (issuer DID, <code>kid</code>), check <code>exp</code>, <code>one_time</code> / <code>use_limit</code>, <code>origin</code>, and (if present) <code>aud</code>.</li>\n<li>Start <strong>authcrypt</strong> DIDComm to one of the provided <code>service.endpoint</code>s using ECDH with the <strong>ephemeral <code>prekey</code></strong>.</li>\n<li>Send the first protocol message (<code>propose</code> for mesh or <code>join-room</code> for SFU) with header <code>oob_thid</code> (below).</li>\n</ol>\n<h3 id=\"OOB-acknowledgment-header\">OOB acknowledgment header</h3>\n<p>First on-wire message following an OOB link MUST include:</p>\n<pre><code>headers.oob_thid = &lt;the invitation thid&gt;\n</code></pre>\n<p>This lets the inviter/SFU invalidate the token and enforce <code>use_limit</code>.</p>\n<h3 id=\"P2P-OOB-flow-mesh\">P2P OOB flow (mesh)</h3>\n<ol>\n<li>Alice generates <code>invite-url</code> (short TTL, one-time) with <code>service</code> endpoints + TURN/STUN + <code>prekey</code>.</li>\n<li>Bob clicks link → verifies → sends <code>propose</code> (with <code>oob_thid</code>) <strong>directly</strong> to Alice’s endpoint.</li>\n<li>Proceed with normal <code>offer</code>/<code>answer</code> + <code>ice</code>.</li>\n</ol>\n<h3 id=\"SFU-OOB-flow\">SFU OOB flow</h3>\n<ol>\n<li>Host creates room via <code>create-room</code>, then mints <code>invite-url</code> containing <code>sfu.endpoint</code>, <code>room_id</code>, <code>prekey</code>, TURN/STUN.</li>\n<li>Participant clicks link → verifies → sends <code>join-room</code> (with <code>oob_thid</code>) <strong>directly</strong> to SFU/host’s endpoint.</li>\n<li>Continue with <code>room-offer</code>/<code>room-answer</code> + <code>ice</code>.</li>\n</ol>\n<p><strong>Security notes</strong></p>\n<ul>\n<li>Use fragments (<code>#oob=</code>) for HTTPS to keep secrets out of server logs.</li>\n<li>Treat <code>thid</code> as the link token and mark spent on first successful use.</li>\n<li>Rotate TURN REST credentials frequently; avoid long-lived secrets in links.</li>\n</ul>\n<hr />\n<h2 id=\"Message-Reference\">Message Reference</h2>\n<p>All messages use DIDComm v2 with <code>type = https://didcomm.org/webrtc/1.0/&lt;message-name&gt;</code>, standard headers (<code>id</code>, <code>thid</code>, <code>from</code>, <code>to</code>, <code>expires_time</code>). Use <strong>authcrypt</strong>.</p>\n<blockquote>\n<p>When a session is bootstrapped from an OOB link, the first message MUST include <code>headers.oob_thid</code>.</p>\n</blockquote>\n<h4 id=\"invite-url\">invite-url</h4>\n<p><strong>Purpose</strong>: Embed in links/QRs only; bootstraps direct DIDComm contact.</p>\n<p><strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/invite-url</code>\n<strong>Fields</strong>: As specified in <strong>Out-of-Band Join Links</strong>.</p>\n<hr />\n<h3 id=\"Mesh-Topology-Messages\">Mesh Topology Messages</h3>\n<h4 id=\"propose\">propose</h4>\n<p>Caller initiates a WebRTC session request.</p>\n<p><strong>From</strong>: caller → callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"media\": [\"audio\",\"video\"],\n  \"data\": true,\n  \"topology\": \"mesh\",\n  \"trickle\": true,\n  \"policy\": \"relay-preferred\",\n  \"ice_servers\": [\n    {\"urls\": [\"stun:stun.l.example.net:3478\"]},\n    {\"urls\": [\"turns:turn.l.example.net:5349\"], \"username\": \"u\", \"credential\": \"p\"}\n  ]\n}\n</code></pre>\n<p><strong>Headers (if OOB)</strong>: <code>{ \"oob_thid\": \"&lt;thid-from-invite&gt;\" }</code></p>\n<hr />\n<h4 id=\"offer\">offer</h4>\n<p><strong>From</strong>: caller → callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"sdp\": \"&lt;SDP offer string&gt;\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 12:34:...:ab\"],\n    \"dtls_role\": \"actpass\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:alice#key-1\",\n    \"created\": 1731140000,\n    \"expires\": 1731140300,\n    \"jws\": \"&lt;JWS signature&gt;\"\n  },\n  \"trickle\": true,\n  \"policy\": \"relay-preferred\",\n  \"ice_servers\": [...],\n  \"tie_breaker\": 823746120\n}\n</code></pre>\n<hr />\n<h4 id=\"answer\">answer</h4>\n<p><strong>From</strong>: callee → caller\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"sdp\": \"&lt;SDP answer string&gt;\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 98:76:...:cd\"],\n    \"dtls_role\": \"passive\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:bob#key-1\",\n    \"created\": 1731140005,\n    \"expires\": 1731140305,\n    \"jws\": \"&lt;JWS signature&gt;\"\n  }\n}\n</code></pre>\n<hr />\n<h4 id=\"ice\">ice</h4>\n<p>Exchange trickle ICE candidates.</p>\n<p><strong>From</strong>: caller ↔ callee OR participant ↔ sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"candidate\": \"candidate:0 1 UDP 2122252543 192.168.1.5 60769 typ host ...\",\n  \"sdpMid\": \"0\",\n  \"sdpMLineIndex\": 0,\n  \"endOfCandidates\": false\n}\n</code></pre>\n<hr />\n<h4 id=\"renegotiate\">renegotiate</h4>\n<p><strong>From</strong>: caller ↔ callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"reason\": \"add-screenshare\",\n  \"ice_restart\": false\n}\n</code></pre>\n<hr />\n<h4 id=\"update\">update</h4>\n<p><strong>From</strong>: caller ↔ callee OR participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"action\": \"mute\",\n  \"media_type\": \"audio\",\n  \"value\": true\n}\n</code></pre>\n<hr />\n<h4 id=\"reject\">reject</h4>\n<p><strong>From</strong>: callee → caller\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"reason\": \"incompatible-capabilities\"\n}\n</code></pre>\n<hr />\n<h4 id=\"end\">end</h4>\n<p><strong>From</strong>: caller ↔ callee\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"reason\": \"hangup\",\n  \"scope\": \"peer\"\n}\n</code></pre>\n<hr />\n<h3 id=\"SFU-Topology-Messages\">SFU Topology Messages</h3>\n<h4 id=\"create-room\">create-room</h4>\n<p><strong>From</strong>: host → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"max_participants\": 100,\n  \"features\": { \"simulcast\": true, \"e2ee\": true, \"recording\": false },\n  \"policy\": { \"ice_policy\": \"relay-preferred\", \"codec_preferences\": [\"VP9\",\"VP8\",\"H.264\"] }\n}\n</code></pre>\n<h4 id=\"room-created\">room-created</h4>\n<p><strong>From</strong>: sfu-mediator → host\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sfu_endpoint\": \"wss://mediator.example/sfu/room-xyz\",\n  \"ice_servers\": [\n    {\"urls\": [\"turns:mediator.example:5349\"], \"username\": \"...\", \"credential\": \"...\"}\n  ],\n  \"e2ee_config\": { \"suite\": \"sframe-aes-gcm-256\", \"key_distribution\": \"didcomm\" }\n}\n</code></pre>\n<h4 id=\"invite\">invite</h4>\n<p><strong>From</strong>: host → participant (via mediator routing or <strong>as OOB link</strong>)\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"media\": [\"audio\",\"video\"],\n  \"sfu_endpoint\": \"wss://mediator.example/sfu/room-xyz\"\n}\n</code></pre>\n<h4 id=\"join-room\">join-room</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Headers (if OOB)</strong>: <code>{ \"oob_thid\": \"&lt;thid-from-invite&gt;\" }</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"media\": [\"audio\",\"video\"],\n  \"capabilities\": { \"simulcast\": true, \"max_recv_bitrate\": 5000000, \"codecs\": [\"VP9\",\"VP8\"] }\n}\n</code></pre>\n<h4 id=\"room-offer\">room-offer</h4>\n<p><strong>From</strong>: sfu-mediator → participant\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sdp\": \"&lt;SDP with receive-only tracks&gt;\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 ...\"],\n    \"dtls_role\": \"actpass\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:sfu#key-1\",\n    \"created\": 1731140010,\n    \"expires\": 1731140310,\n    \"jws\": \"...\"\n  },\n  \"participants\": [\n    {\n      \"did\": \"did:example:alice\",\n      \"stream_id\": \"stream-alice-1\",\n      \"media\": [\"audio\",\"video\"],\n      \"e2ee_key_id\": \"kid-alice-123\"\n    }\n  ]\n}\n</code></pre>\n<h4 id=\"room-answer\">room-answer</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sdp\": \"&lt;SDP answer with send-only tracks&gt;\",\n  \"fingerprint_proof\": {\n    \"fingerprints\": [\"sha-256 ...\"],\n    \"dtls_role\": \"passive\",\n    \"alg\": \"EdDSA\",\n    \"kid\": \"did:example:bob#key-1\",\n    \"created\": 1731140011,\n    \"expires\": 1731140311,\n    \"jws\": \"...\"\n  }\n}\n</code></pre>\n<h4 id=\"participant-joined\">participant-joined</h4>\n<p><strong>From</strong>: sfu-mediator → all participants\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"participant\": { \"did\": \"did:example:bob\", \"stream_id\": \"stream-bob-1\", \"media\": [\"audio\",\"video\"] }\n}\n</code></pre>\n<h4 id=\"leave-room\">leave-room</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"reason\": \"user-left\"\n}\n</code></pre>\n<h4 id=\"participant-left\">participant-left</h4>\n<p><strong>From</strong>: sfu-mediator → all participants\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"participant\": \"did:example:bob\",\n  \"reason\": \"user-left\"\n}\n</code></pre>\n<h4 id=\"quality-update\">quality-update</h4>\n<p><strong>From</strong>: sfu-mediator → participant\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"updates\": [{ \"stream_id\": \"stream-alice-1\", \"spatial_layer\": \"medium\", \"temporal_layer\": 2, \"max_bitrate\": 1500000 }]\n}\n</code></pre>\n<h4 id=\"simulcast-control\">simulcast-control</h4>\n<p><strong>From</strong>: participant → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"preferences\": { \"stream-alice-1\": \"high\", \"stream-charlie-1\": \"low\" }\n}\n</code></pre>\n<h4 id=\"destroy-room\">destroy-room</h4>\n<p><strong>From</strong>: host → sfu-mediator\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"reason\": \"host-ended\"\n}\n</code></pre>\n<h4 id=\"ping--pong\">ping / pong</h4>\n<p><strong>From</strong>: any ↔ any\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{ \"timestamp\": 1698765432000 }\n</code></pre>\n<hr />\n<h2 id=\"End-to-End-Encryption-SFU-Mode\">End-to-End Encryption (SFU Mode)</h2>\n<h3 id=\"Model\">Model</h3>\n<p>In SFU topology, media traverses the mediator. E2EE is achieved with <strong>SFrame</strong>, which encrypts encoded frames at the application layer using a <strong>symmetric “sender key” per sender</strong>.</p>\n<p><strong>Key properties</strong></p>\n<ul>\n<li>Each sender maintains a symmetric SFrame key identified by <code>kid</code>.</li>\n<li>Frames are encrypted before leaving the client and decrypted after reception.</li>\n<li>The SFU forwards opaque ciphertext; it never sees plaintext.</li>\n</ul>\n<h3 id=\"Key-Distribution-strictly-end-to-end\">Key Distribution (strictly end-to-end)</h3>\n<ol>\n<li>\n<p><strong>Sender creates</strong> a new random symmetric SFrame key (<code>kid</code>), e.g., on join or rotation.</p>\n</li>\n<li>\n<p><strong>Sender wraps</strong> that key <strong>for each recipient</strong> using recipients’ DID public keys (HPKE/ECDH-ES → AEAD).</p>\n</li>\n<li>\n<p><strong>Sender distributes</strong> wrapped keys via DIDComm:</p>\n<ul>\n<li><code>key-announce</code> (new sender key)</li>\n<li><code>key-rotate</code> (periodic or on membership change)</li>\n<li><code>key-revoke</code> (e.g., when removing a participant)</li>\n</ul>\n</li>\n<li>\n<p><strong>Recipients unwrap</strong> and install the key mapped to <code>sender DID + kid</code>.</p>\n</li>\n</ol>\n<blockquote>\n<p><strong>The SFU MUST NOT generate, unwrap, or rewrap sender keys.</strong> It may relay the DIDComm key messages but never handle key plaintext.</p>\n</blockquote>\n<h3 id=\"Key-management-messages\">Key management messages</h3>\n<h4 id=\"key-announce\">key-announce</h4>\n<p><strong>From</strong>: sender → room (via SFU routing is OK; remains E2E)\n<strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/key-announce</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sender\": \"did:example:alice\",\n  \"suite\": \"sframe-aes-gcm-256\",\n  \"kid\": \"kid-alice-123\",\n  \"wrapped_keys\": [\n    { \"recipient\": \"did:example:bob#key-1\", \"jwe\": \"&lt;HPKE/JWE&gt;\" },\n    { \"recipient\": \"did:example:charlie#key-1\", \"jwe\": \"&lt;HPKE/JWE&gt;\" }\n  ],\n  \"created\": 1731140020\n}\n</code></pre>\n<h4 id=\"key-rotate\">key-rotate</h4>\n<p><strong>From</strong>: sender → room\n<strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/key-rotate</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sender\": \"did:example:alice\",\n  \"old_kid\": \"kid-alice-123\",\n  \"new_kid\": \"kid-alice-124\",\n  \"wrapped_keys\": [ /* as above for new_kid */ ],\n  \"reason\": \"membership-change\"\n}\n</code></pre>\n<h4 id=\"key-revoke\">key-revoke</h4>\n<p><strong>From</strong>: host/sender → room\n<strong>Type</strong>: <code>https://didcomm.org/webrtc/1.0/key-revoke</code>\n<strong>Body</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"room_id\": \"room-xyz\",\n  \"sender\": \"did:example:alice\",\n  \"kid\": \"kid-alice-123\",\n  \"reason\": \"participant-removed\"\n}\n</code></pre>\n<h3 id=\"Insertable-Streams-illustrative\">Insertable Streams (illustrative)</h3>\n<pre><code class=\"language-javascript\">// Encrypt outgoing frames with current sender key\nconst tx = new TransformStream({\n  transform: (encoded, ctl) =&gt; ctl.enqueue(sframeEncrypt(encoded, senderKey))\n});\nsender.transform = tx;\n\n// Decrypt incoming frames using sender DID + kid\nconst rx = new TransformStream({\n  transform: (encoded, ctl) =&gt; {\n    const key = keyStore.get(encoded.meta.senderDid, encoded.meta.kid);\n    ctl.enqueue(sframeDecrypt(encoded, key));\n  }\n});\nreceiver.transform = rx;\n</code></pre>\n<hr />\n<h2 id=\"Flows\">Flows</h2>\n<h3 id=\"Basic-Two-Party-Call-Mesh\">Basic Two-Party Call (Mesh)</h3>\n<ol>\n<li><strong>Alice → Bob</strong>: <code>propose</code> (media types, ICE policy)</li>\n<li><strong>Alice</strong>: Create <code>RTCPeerConnection</code>, generate SDP offer, sign fingerprint proof</li>\n<li><strong>Alice → Bob</strong>: <code>offer</code> (SDP + <code>fingerprint_proof</code>)</li>\n<li><strong>Bob</strong>: Verify proof (DID doc); set remote; create answer; sign fingerprint</li>\n<li><strong>Bob → Alice</strong>: <code>answer</code> (SDP + <code>fingerprint_proof</code>)</li>\n<li><strong>Both</strong>: Exchange <code>ice</code> (trickle); DTLS handshake; media flows P2P</li>\n<li><strong>Either</strong>: <code>end</code> to terminate</li>\n</ol>\n<h3 id=\"Multi-Party-Call-with-SFU-50-participants\">Multi-Party Call with SFU (50 participants)</h3>\n<ol>\n<li><strong>Host → SFU</strong>: <code>create-room</code> (E2EE enabled)</li>\n<li><strong>SFU → Host</strong>: <code>room-created</code> (room_id, sfu_endpoint)</li>\n<li><strong>Host → invitees</strong>: <code>invite</code> <strong>or OOB links</strong></li>\n<li><strong>Participant → SFU</strong>: <code>join-room</code> (capabilities)</li>\n<li><strong>SFU → Participant</strong>: <code>room-offer</code></li>\n<li><strong>Participant → SFU</strong>: <code>room-answer</code></li>\n<li><strong>Sender(s)</strong>: <code>key-announce</code> (SFrame sender keys) via DIDComm</li>\n<li><strong>Everyone</strong>: trickle <code>ice</code>; media flows via SFU (still E2EE)</li>\n</ol>\n<h3 id=\"OOB-Link-P2P\">OOB Link (P2P)</h3>\n<ol>\n<li><strong>Alice</strong> creates <code>invite-url</code> (short TTL, one-time, <code>prekey</code>).</li>\n<li><strong>Bob</strong> clicks, verifies, and sends <code>propose</code> <strong>directly</strong> to Alice (<code>oob_thid</code> set).</li>\n<li>Continue with <code>offer</code>/<code>answer</code>/<code>ice</code>.</li>\n</ol>\n<h3 id=\"OOB-Link-SFU\">OOB Link (SFU)</h3>\n<ol>\n<li><strong>Host</strong> creates room, mints <code>invite-url</code> (includes <code>sfu.endpoint</code>, <code>room_id</code>, <code>prekey</code>).</li>\n<li><strong>Participant</strong> clicks, verifies, and sends <code>join-room</code> to SFU (<code>oob_thid</code> set).</li>\n<li>Continue with <code>room-offer</code>/<code>room-answer</code>/<code>ice</code>, then E2EE key announcements.</li>\n</ol>\n<h3 id=\"Glare-Resolution-Mesh\">Glare Resolution (Mesh)</h3>\n<p>If both sides send <code>offer</code> simultaneously:</p>\n<ol>\n<li>Compare <code>tie_breaker</code>; the lower value <strong>retracts</strong> and answers the other.</li>\n<li>DTLS <code>setup</code> roles SHOULD follow JSEP perfect-negotiation norms.</li>\n</ol>\n<h3 id=\"Dynamic-Quality-Adaptation-SFU\">Dynamic Quality Adaptation (SFU)</h3>\n<ol>\n<li>SFU detects congestion.</li>\n<li><strong>SFU → Participant</strong>: <code>quality-update</code> (layer caps).</li>\n<li>Client adjusts simulcast/SVC subscriptions and encodings.</li>\n</ol>\n<h3 id=\"Using-with-Message-Pickup\">Using with Message Pickup</h3>\n<p>When callee is offline:</p>\n<ol>\n<li>Caller’s <code>propose</code>/<code>offer</code> queued at mediator (mesh), or <code>invite</code> queued.</li>\n<li>Callee polls via Message Pickup 4.0.</li>\n<li>Callee processes and continues as usual.</li>\n</ol>\n<hr />\n<h2 id=\"Security-Considerations\">Security Considerations</h2>\n<h3 id=\"Authentication--Integrity\">Authentication &amp; Integrity</h3>\n<ul>\n<li>\n<p><strong>DIDComm authcrypt</strong> for all signaling messages.</p>\n</li>\n<li>\n<p><strong>Fingerprint binding</strong>: <code>fingerprint_proof</code> JWS binds WebRTC DTLS certificate(s) to a DID key.</p>\n<ul>\n<li>Use <code>alg: \"EdDSA\"</code> with <code>kid</code> referencing a DID URL.</li>\n<li>Sign a canonical payload containing: <code>thid</code>, <strong>all</strong> <code>a=fingerprint</code> values, <code>dtls_role</code>, <code>created</code>, <code>expires</code>.</li>\n<li>Verifier checks freshness, DID key validity, and exact SDP fingerprint match.</li>\n</ul>\n</li>\n</ul>\n<h3 id=\"OOB-Link-Security\">OOB Link Security</h3>\n<ul>\n<li><strong>Short TTLs</strong> and <strong>single-use</strong> (<code>one_time</code> or <code>use_limit</code>) to mitigate link leaks.</li>\n<li><strong>Audience binding</strong> (<code>constraints.aud</code>) if the invitee DID is known.</li>\n<li><strong>Origin binding</strong> (<code>constraints.origin</code>) for web joins; keep <code>_oob</code> in the URL <strong>fragment</strong>.</li>\n<li><strong>Ephemeral prekeys</strong> per link; never reuse; do not expose long-term keys.</li>\n</ul>\n<h3 id=\"Privacy\">Privacy</h3>\n<ul>\n<li>Pairwise DIDs to prevent correlation across sessions.</li>\n<li><code>relay-only</code> ICE policy to hide IPs (at higher cost/latency).</li>\n<li>Prefer mDNS candidates over private IP exposure.</li>\n<li>Minimize metadata in headers and <code>reason</code> fields.</li>\n</ul>\n<h3 id=\"Media-Encryption\">Media Encryption</h3>\n<ul>\n<li>Mesh: DTLS-SRTP provides E2EE by default.</li>\n<li>SFU: <strong>SFrame</strong> provides application-layer E2EE; keys are <strong>symmetric</strong> and distributed <strong>end-to-end</strong> by clients.</li>\n</ul>\n<h3 id=\"Problem-Reports-Report-Problem-20-codes\">Problem Reports (Report Problem 2.0 codes)</h3>\n<ul>\n<li><code>incompatible-capabilities</code>, <code>ice-gathering-timeout</code>, <code>turn-auth-failed</code>,\n<code>unverified-fingerprint</code>, <code>glare-timeout</code>, <code>policy-required</code>,\n<code>topology-unsupported</code>, <code>room-full</code>, <code>sfu-unavailable</code>.</li>\n</ul>\n<hr />\n<h2 id=\"Interop-Notes\">Interop Notes</h2>\n<h3 id=\"DID-Document-Service-Endpoints-Optional\">DID Document Service Endpoints (Optional)</h3>\n<p>Agents MAY publish WebRTC ICE server configuration:</p>\n<pre><code class=\"language-json\">{\n  \"id\": \"#webrtc-ice\",\n  \"type\": \"webrtc-ice-servers\",\n  \"serviceEndpoint\": \"https://agent.alice.example/ice\",\n  \"accept\": [\"webrtc/ice-servers/1.0\"]\n}\n</code></pre>\n<p>SFU mediators MAY publish SFU capabilities:</p>\n<pre><code class=\"language-json\">{\n  \"id\": \"#webrtc-sfu\",\n  \"type\": \"webrtc-sfu\",\n  \"serviceEndpoint\": \"wss://mediator.example/sfu\",\n  \"capabilities\": {\n    \"max_participants\": 100,\n    \"simulcast\": true,\n    \"codecs\": [\"VP9\",\"VP8\",\"H.264\"],\n    \"e2ee\": [\"sframe\"]\n  }\n}\n</code></pre>\n<h3 id=\"Composition-with-Other-Protocols\">Composition with Other Protocols</h3>\n<ul>\n<li><strong>Discover Features 2.0</strong>: Capability negotiation</li>\n<li><strong>Message Pickup 4.0</strong>: Async signaling when offline</li>\n<li><strong>Coordinate Mediation 3.0</strong>: DID registration with mediators</li>\n<li><strong>Report Problem 2.0</strong>: Error reporting</li>\n<li><strong>Vaults 1.0</strong>: Recording storage (encrypted)</li>\n</ul>\n<h3 id=\"Platform-Considerations\">Platform Considerations</h3>\n<ul>\n<li><strong>Browser</strong>: <code>RTCPeerConnection</code> + Insertable Streams for SFrame</li>\n<li><strong>Mobile</strong>: Native WebRTC libs (iOS WebRTC.framework, Android libwebrtc)</li>\n<li><strong>Rust</strong>: <code>webrtc-rs</code> (or equivalents)</li>\n<li><strong>Node.js</strong>: <code>wrtc</code> / <code>node-webrtc</code></li>\n</ul>\n<hr />\n<h2 id=\"Implementation-Hints\">Implementation Hints</h2>\n<h3 id=\"Fingerprint-Extraction--Proof\">Fingerprint Extraction &amp; Proof</h3>\n<pre><code class=\"language-javascript\">function extractFingerprints(sdp) {\n  return [...sdp.matchAll(/^a=fingerprint:(\\S+)\\s+(.+)$/gm)]\n         .map(m =&gt; `${m[1]} ${m[2]}`);\n}\n\nfunction createFingerprintProof({ thid, fingerprints, dtls_role, didKey }) {\n  const payload = { thid, fingerprints, dtls_role, created: Date.now(), expires: Date.now()+300000 };\n  return signJWS(payload, didKey, { alg: \"EdDSA\", kid: didKey.kid });\n}\n</code></pre>\n<h3 id=\"Trickle-ICE\">Trickle ICE</h3>\n<pre><code class=\"language-javascript\">pc.onicecandidate = (e) =&gt; {\n  sendDIDComm({\n    type: \"https://didcomm.org/webrtc/1.0/ice\",\n    thid: sessionId,\n    body: e.candidate ? {\n      candidate: e.candidate.candidate,\n      sdpMid: e.candidate.sdpMid,\n      sdpMLineIndex: e.candidate.sdpMLineIndex,\n      endOfCandidates: false\n    } : { endOfCandidates: true }\n  });\n};\n</code></pre>\n<h3 id=\"OOB-Link-Validation-Receiver\">OOB Link Validation (Receiver)</h3>\n<pre><code class=\"language-javascript\">async function handleOOB(oob) {\n  const { body } = oob;\n  assert(Date.now()/1000 &lt; body.exp);\n  await verifyJWS(body.proof.jws, body.proof.kid); // EdDSA over canonical body\n  // Start DIDComm to body.service[0].endpoint using ECDH with body.prekey\n}\n</code></pre>\n<h3 id=\"SFU-Implementation-Hints\">SFU Implementation Hints</h3>\n<ul>\n<li>ICE-Lite recommended; forward RTP with header extensions preserved for SFrame.</li>\n<li>Respect client <code>simulcast-control</code> requests and issue <code>quality-update</code> as needed.</li>\n<li>Never unwrap or generate SFrame keys; only relay key messages.</li>\n</ul>\n<hr />\n<h2 id=\"References\">References</h2>\n<ul>\n<li>WebRTC 1.0: Real-Time Communication Between Browsers — <a href=\"https://www.w3.org/TR/webrtc/\">https://www.w3.org/TR/webrtc/</a></li>\n<li>RFC 8829 — JavaScript Session Establishment Protocol (JSEP)</li>\n<li>RFC 8445 — Interactive Connectivity Establishment (ICE)</li>\n<li>SFrame — IETF draft: draft-ietf-sframe-enc</li>\n<li>Insertable Streams for MediaStreamTrack — <a href=\"https://w3c.github.io/webrtc-encoded-transform/\">https://w3c.github.io/webrtc-encoded-transform/</a></li>\n<li>DIDComm Messaging Specification v2.0 — <a href=\"https://identity.foundation/didcomm-messaging/spec/v2.0/\">https://identity.foundation/didcomm-messaging/spec/v2.0/</a></li>\n<li>Discover Features Protocol 2.0 — <a href=\"https://didcomm.org/discover-features/2.0/\">https://didcomm.org/discover-features/2.0/</a></li>\n<li>Message Pickup Protocol 4.0 — <a href=\"https://didcomm.org/messagepickup/4.0/\">https://didcomm.org/messagepickup/4.0/</a></li>\n<li>Coordinate Mediation Protocol 3.0 — <a href=\"https://didcomm.org/coordinate-mediation/3.0/\">https://didcomm.org/coordinate-mediation/3.0/</a></li>\n<li>Vaults Protocol 1.0 — <a href=\"https://didcomm.org/vaults/1.0/\">https://didcomm.org/vaults/1.0/</a></li>\n</ul>\n<hr />"}},
    "staticQueryHashes": ["3000541721","3766577012"],
    "matchPath": "webrtc/1.0/*"}