diff --git a/proposals/4388-secure-qr-channel.md b/proposals/4388-secure-qr-channel.md
new file mode 100644
index 00000000000..8e16bd5df26
--- /dev/null
+++ b/proposals/4388-secure-qr-channel.md
@@ -0,0 +1,1162 @@
+# MSC4388: Secure out-of-band channel for sign in with QR
+
+This proposal forms part of [MSC4108] to make it easy to sign in on a new device with the help of an existing device.
+
+It proposes a mechanism for a new and an existing device to establish a secure out-of-band channel through which they can
+communicate to facilitate the sign in of the new device.
+
+Table of contents:
+
+- [Proposal](#proposal)
+- [Insecure rendezvous session](#insecure-rendezvous-session)
+- [QR code format](#qr-code-format)
+- [Secure channel](#secure-channel)
+- [Potential issues](#potential-issues)
+- [Alternatives](#alternatives)
+- [Security considerations](#security-considerations)
+- [Unstable prefix](#unstable-prefix)
+- [Dependencies](#dependencies)
+
+## Proposal
+
+Depending on the pair of devices used, it may be preferable to scan the QR code on either the new or existing device,
+based on the availability of a camera. As such, this proposal allows for the generation of the QR on either device.
+
+This proposal is split into three parts:
+
+- Creating an insecure rendezvous session on a homeserver
+- Encoding the rendezvous session and an ephemeral key in a QR code
+- Establishing a secure channel over the insecure rendezvous session using an ephemeral key
+
+The overall flow looks like this:
+
+```mermaid
+sequenceDiagram
+ participant G as Device generating QR
+ participant HS as Homeserver
+ participant S as Device scanning QR
+
+ note over G,S: Generating and scanning devices each pick ephemeral Curve25519 keys to use to set up the secure channel
+
+ note over G: Generating device picks a suitable homeserver to use for the rendezvous session
+
+ G->>+HS: Creates a rendezvous session on the homeserver
+ HS->>-G: ID for the rendezvous
+
+ note over G: Generates and displays a QR code containing:
ephemeral key of generating device, rendezvous ID, homeserver base URL,
device disposition (whether device is new or existing)
+
+ G-->>S: Other device scans the QR code
+
+ note over S: Scanning device checks that the disposition matches what it expects
+
+ S->>+HS: Scanning device checks that it can communicate with the
rendezvous session on the homeserver identified by information in QR
+ HS->>-S: OK
+
+ G<<->>S: Generating and scanning device then communicate by
sending and receiving to/from the rendezvous session via the homeserver
+
+ note over G,S: Devices perform Elliptic Curve Diffie-Hellman protocol
(using the ephemeral keys from earlier on) to agree a shared secret
+ S->>G: Scanning device sends LoginInitiateMessage handshake message
+ G->>S: Generating device validates and responds with LoginOkMessage handshake message
+
+ note over G,S: Devices ask user to confirm that the connection isn't being Man-In-The-Middle attacked by
entering a two digit CheckCode shown on scanning device into the generating device
+
+ S-->>G: User enters two digit code
+ note over G,S: Devices can now communicate securely using the shared secret to encrypt future messages via the rendezvous session
+
+ S<<->>G: Devices then send and receive messages as described in MSC4108
+```
+
+## Insecure rendezvous session
+
+It is proposed that an HTTP-based protocol be used to establish an ephemeral bi-directional communication session over
+which the two devices can exchange the necessary data.
+
+This session is described as "insecure" as it provides no
+end-to-end confidentiality nor authenticity by itself - these are layered on top of it.
+
+The name "rendezvous" is used as it is the designated place where the two clients will meet.
+
+New optional HTTP endpoints are to be added to the Client-Server API.
+
+### Concept
+
+Suppose that Device A wants to establish communications with Device B. Device A can do so by creating a
+_rendezvous session_ via a `POST /_matrix/client/v1/rendezvous` call to an appropriate homeserver. Its response includes
+a _rendezvous ID_ which, along with the server [base URL], should be shared out-of-band with Device B.
+
+The rendezvous ID points to an arbitrary data resource (the "payload") on the homeserver, which is initially populated
+using data from A's initial `POST` request. The payload is a string which the homeserver must enforce a maximum length on.
+
+The maximum length of a rendezvous ID is 65,535 bytes.
+
+Note that the rendezvous session is not a channel that two clients can use to send a sequence of messages between them,
+but rather a single shared mutable spot whose contents can be inspected and overwritten by either party. Each new write
+replaces the previous payload entirely; the homeserver retains no history of prior payloads.
+
+Anyone who is able to reach the homeserver and has the rendezvous ID - including: Device A; Device B; or a third party; -
+can then "receive" the payload by polling via a `GET` request, and "send" a new payload by making a `PUT` request.
+
+In this way, Device A and Device B can communicate by repeatedly inspecting and updating the payload at the rendezvous session.
+
+This has consequences for how clients use the session: a client cannot tell whether its previous payload has been
+received by the remote client until that remote client itself writes a new payload and the original client observes the
+change. This favours a "ping-pong" architecture, where a client sends a message, waits for a reply, and only then sends
+its next message. If a client needs to send several messages in a row without an intervening reply, it should update
+the payload to contain a list of those messages rather than overwriting it repeatedly.
+
+### The send mechanism
+
+Every send (`PUT`) request MUST include a `sequence_token` value whose value is the `sequence_token` from the last `GET`
+response seen by the requester. (The initiating device may also use the `sequence_token` supplied in the initial `POST` response
+to immediately update the payload.) Sends will succeed only if the supplied `sequence_token` matches the server's current
+revision of the payload. This prevents concurrent writes to the payload.
+
+To make sends idempotent (so that clients can safely retry a request whose response was lost), the server MUST also
+accept a send request whose `sequence_token` does not match the current revision if the supplied `data` is byte-for-byte
+identical to the current payload. In that case the server MUST NOT advance the payload or generate a new
+`sequence_token`, and MUST return the current `sequence_token` in the response, as if the client's previous (successful)
+request were being acknowledged again. Any other mismatch of `sequence_token` MUST be rejected as a concurrent write.
+
+n.b. Once a new payload has been sent there is no mechanism to retrieve previous payloads.
+
+The maximum length of a `sequence_token` is 65,535 bytes.
+
+### Expiry
+
+The rendezvous session (i.e. the payload) SHOULD expire after a period of time communicated to clients via the
+`expires_in_ms` field on the `POST` and `GET` response bodies. After this point, any further attempts to query or update
+the payload MUST fail. The rendezvous session can be manually expired with a `DELETE` call to the rendezvous session.
+
+### `GET /_matrix/client/v1/rendezvous` - Discover if the rendezvous API is available
+
+Rate-limited: Yes
+Requires authentication: Optional - depending on server policy
+
+Clients can use this endpoint to determine if the rendezvous API is available to them. Because the server policy may
+require authentication, clients should make this request with their access token if they have one.
+
+HTTP response codes, and Matrix error codes:
+
+- `200 OK` - rendezvous API discovery supported
+- `403 Forbidden` (`M_FORBIDDEN`) - the requester is not authorized to use the rendezvous API
+- `404 Not Found` (`M_UNRECOGNIZED`) - the rendezvous API is not enabled
+- `429 Too Many Requests` (`M_LIMIT_EXCEEDED`) - the request has been rate limited
+
+The response body for `200 OK` is `application/json` with contents:
+
+|Field|Type||
+|-|-|-|
+|`create_available`|required `boolean`|`true` if the requester is able to create a rendezvous session using `POST /_matrix/client/v1/rendezvous` otherwise `false`|
+
+Example response:
+
+```http
+HTTP 200 OK
+Content-Type: application/json
+
+{
+ "create_available": true
+}
+```
+
+The body could be extended in future to provide any other information that the requester might require to use the
+rendezvous API.
+
+The server can chose what level of authentication is required to create a rendezvous session. Please see the description
+of `POST /_matrix/client/v1/rendezvous` for a description of this.
+
+### `POST /_matrix/client/v1/rendezvous` - Create a rendezvous session and send initial payload
+
+Rate-limited: Yes
+Requires authentication: Optional - depending on server policy
+
+Request body is `application/json` with contents:
+
+|Field|Type||
+|-|-|-|
+|`data`|required `string`|The data payload to be sent|
+
+For example:
+
+```http
+POST /_matrix/client/v1/rendezvous HTTP/1.1
+Content-Type: application/json
+
+{
+ "data": "initial data"
+}
+```
+
+HTTP response codes, and Matrix error codes:
+
+- `200 OK` - rendezvous session created
+- `403 Forbidden` (`M_FORBIDDEN`) - the requester is not authorized to create the rendezvous session
+- `404 Not Found` (`M_UNRECOGNIZED`) - the rendezvous API is not enabled
+- `413 Payload Too Large` (`M_TOO_LARGE`) - the supplied `data` value is larger than the 4096 byte limit
+- `429 Too Many Requests` (`M_LIMIT_EXCEEDED`) - the request has been rate limited
+
+Response body for `200 OK` is `application/json` with contents:
+
+|Field|Type||
+|-|-|-|
+|`id`|required `string`|Opaque identifier for the rendezvous session. Maximum length 65,535 bytes|
+|`sequence_token`|required `string`|The opaque token to identify if the payload has changed. Maximum length 65,535 bytes|
+|`expires_in_ms`|required `integer`|The number of milliseconds remaining until the rendezvous session expires|
+
+Example response:
+
+```http
+HTTP 200 OK
+Content-Type: application/json
+
+{
+ "id": "abcdEFG12345",
+ "sequence_token": "VmbxF13QDusTgOCt8aoa0d2PQcnBOXeIxEqhw5aQ03o=",
+ "expires_in_ms": 300000
+}
+```
+
+The server can chose what level of authentication is required to create a rendezvous session. Suitable policies might
+include:
+
+- Requires authenticated user - this would reduce abuse to known users. This should be the default.
+- Public/open - anyone can create a rendezvous session without an access token. This allows for clients to use this
+ server as its default when creating QR codes as a new session (as it doesn't yet know what server the user will
+ log in to).
+
+The expiry time is detailed [below](#maximum-duration-of-a-rendezvous).
+
+### `PUT /_matrix/client/v1/rendezvous/{rendezvousId}` - Send a payload to an existing rendezvous
+
+Rate-limited: Yes
+Requires authentication: No
+
+Request body is `application/json` with contents:
+
+|Field|Type||
+|-|-|-|
+|`sequence_token`|required `string`|The value of `sequence_token` from the last payload seen by the requesting device.|
+|`data`|required `string`|The data payload to be sent.|
+
+For example:
+
+```http
+PUT /_matrix/client/v1/rendezvous/abcdEFG12345 HTTP/1.1
+Content-Type: application/json
+
+{
+ "sequence_token": "VmbxF13QDusTgOCt8aoa0d2PQcnBOXeIxEqhw5aQ03o=",
+ "data": "new data"
+}
+```
+
+The server MUST perform a compare-and-swap operation by checking that the `sequence_token` matches
+the current sequence token for the session. If the `sequence_token` does not match then the `data` MUST not be
+accepted and the `M_CONCURRENT_WRITE` error is returned. On receipt of a `M_CONCURRENT_WRITE` the client can do a `GET`
+to fetch the latest data and `sequence_token` and then retry.
+
+To support idempotent retries (e.g. when a client did not receive the response to a previous `PUT` and so does not know
+whether it succeeded), the server MUST treat the request as successful - without advancing the payload or issuing a new
+`sequence_token` - if the supplied `sequence_token` does not match the current sequence token but the supplied `data`
+is byte-for-byte identical to the current payload. In this case the server returns `200 OK` with the current
+`sequence_token`, rather than `M_CONCURRENT_WRITE`. This allows a client to safely repeat the same `PUT` request without
+first issuing a `GET`.
+
+HTTP response codes, and Matrix error codes:
+
+- `200 OK` - payload updated
+- `404 Not Found` (`M_NOT_FOUND`) - rendezvous session ID is not valid (it could have expired)
+- `404 Not Found` (`M_UNRECOGNIZED`) - the rendezvous API is not enabled
+- `409 Conflict` (`M_CONCURRENT_WRITE`, a new error code) - when the `sequence_token` does not match
+- `413 Payload Too Large` (`M_TOO_LARGE`) - the supplied `data` value is larger than the 4096 byte limit
+- `429 Too Many Requests` (`M_LIMIT_EXCEEDED`) - the request has been rate limited
+
+The response body for `200 OK` is `application/json` with contents:
+
+|Field|Type||
+|-|-|-|
+|`sequence_token`|required `string`|The opaque token to identify if the payload has changed. Maximum length 65,535 bytes|
+
+For example:
+
+```http
+HTTP 200 OK
+Content-Type: application/json
+
+{
+ "sequence_token": "VmbxF13QDusTgOCt8aoa0d2PQcnBOXeIxEqhw5aQ03o="
+}
+```
+
+### `GET /_matrix/client/v1/rendezvous/{rendezvousId}` - Receive a payload from a rendezvous session
+
+Rate-limited: Yes
+Requires authentication: No
+
+HTTP response codes, and Matrix error codes:
+
+- `200 OK` - payload returned
+- `403 Forbidden` (`M_FORBIDDEN`) - request is not allowed due to the unsafe content policy (see below)
+- `404 Not Found` (`M_NOT_FOUND`) - rendezvous session ID is not valid (it could have expired)
+- `404 Not Found` (`M_UNRECOGNIZED`) - the rendezvous API is not enabled
+- `429 Too Many Requests` (`M_LIMIT_EXCEEDED`) - the request has been rate limited
+
+Response body for `200 OK` is `application/json` with contents:
+
+|Field|Type||
+|-|-|-|
+|`data`|required `string`|The data payload from the last POST or PUT.|
+|`sequence_token`|required `string`|The token opaque token to identify if the payload has changed|
+|`expires_in_ms`|required `integer`|The number of milliseconds remaining until the rendezvous session expires|
+
+```http
+HTTP 200 OK
+Content-Type: application/json
+
+{
+ "data": "data from the previous POST/PUT",
+ "sequence_token": "VmbxF13QDusTgOCt8aoa0d2PQcnBOXeIxEqhw5aQ03o=",
+ "expires_in_ms": 300000
+}
+```
+
+To help mitigate the threat of [unsafe content](#unsafe-content), the server SHOULD inspect the `Sec-Fetch-*`
+[Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) (or other suitable headers) to identify
+top-level navigation requests and return a `403` HTTP response with error code `M_FORBIDDEN` instead.
+
+The exact header values to use are an implementation detail for the server implementation.
+
+A future optimisation could be allow the client to "long-poll" by sending the previous `sequence_token` as a query parameter
+and then the server returns when there is new data or some timeout has passed.
+
+### `DELETE /_matrix/client/v1/rendezvous/{rendezvousId}` - cancel a rendezvous session
+
+Rate-limited: Yes
+Requires authentication: No
+
+HTTP response codes:
+
+- `200 OK` - rendezvous session cancelled
+- `404 Not Found` (`M_NOT_FOUND`) - rendezvous session ID is not valid (it could have expired)
+- `404 Not Found` (`M_UNRECOGNIZED`) - the rendezvous API is not enabled
+- `429 Too Many Requests` (`M_LIMIT_EXCEEDED`) - the request has been rate limited
+
+Cancelling a session will cause all future reads and writes to fail with a `404 M_NOT_FOUND`.
+
+### Example API usage
+
+The actions above can be illustrated as follows:
+
+```mermaid
+sequenceDiagram
+ participant A as Device A
+ participant HS as Homeserver
https://matrix.example.com
+ participant B as Device B
+ Note over A: Device A determines which rendezvous server to use
+
+ A->>+HS: POST /_matrix/client/v1/rendezvous
{"data":"Hello from A"}
+ HS->>-A: 200 OK
{"id":"abc-def-123-456","sequence_token": "1", "expires_in_ms": 300000}
+
+ A-->>B: Rendezvous ID and homeserver base URL shared out of band as QR code: e.g. id=abc-def-123-456 baseURL=https://matrix.example.com
+
+ Note over A: Device A starts polling for new payloads at the
rendezvous session using the returned `sequence_token`
+ activate A
+
+ Note over B: Device B resolves the servername to the homeserver
+
+ B->>+HS: GET /_matrix/client/v1/rendezvous/abc-def-123-456
+ HS->>-B: 200 OK
{"sequence_token": "1", "data": "Hello from A"}
+ loop Device A polls the rendezvous session for a new payload
+ A->>+HS: GET /_matrix/client/v1/rendezvous/abc-def-123-456
+ alt is not modified
+ HS->>-A: 200 OK
{"sequence_token": "1", "data": "Hello from A", "expires_in_ms": 300000}
+ end
+ end
+
+ note over B: Device B sends a new payload
+ B->>+HS: PUT /_matrix/client/v1/rendezvous/abc-def-123-456
{"sequence_token": "1", "data": "Hello from B"}
+ HS->>-B: 200 OK
{"sequence_token": "2"}
+
+ Note over B: Device B starts polling for new payloads at the
rendezvous session using the new `sequence_token`
+ activate B
+
+ loop Device B polls the rendezvous session for a new payload
+ B->>+HS: GET /_matrix/client/v1/rendezvous/abc-def-123-456
+ alt is not modified
+ HS->>-B: 200 OK
{"sequence_token": "2", "data": "Hello from A", "expires_in_ms": 300000}
+ end
+ end
+
+ note over A: Device A then receives the new payload
+ opt modified
+ HS->>A: 200 OK
{"sequence_token": "2", "data": "Hello from B", "expires_in_ms": 300000}
+ end
+ deactivate A
+
+ note over A: Device A sends a new payload
+ A->>+HS: PUT /_matrix/client/v1/rendezvous/abc-def-123-456
{"sequence_token": "2", "data": "Hello again from A"}
+ HS->>-A: 200 OK
{"sequence_token": "3"}
+
+ note over B: Device B then receives the new payload
+ opt modified
+ HS->>B: 200 OK
{"sequence_token": "3", "data": "Hello again from B", "expires_in_ms": 300000}
+ end
+
+ deactivate B
+```
+
+### Implementation notes
+
+#### Maximum payload size
+
+The server MUST enforce a maximum `data` field size of 4096 bytes.
+
+#### `sequence_token` values
+
+The `sequence_token` values should be unique to the rendezvous session and the last modified time so that two clients can
+distinguish between identical payloads sent by either client.
+
+#### Maximum duration of a rendezvous
+
+The rendezvous session needs to persist for the duration of the login including allowing the user another time to
+confirm that the secure channel has been established and complete any extra homeserver mandated login steps such as MFA.
+
+Clients should handle the case of the rendezvous session being cancelled or timed out by the server.
+
+The server MUST enforce a timeout on each rendezvous. When picking a value to use:
+
+- the minimum timeout SHOULD be 120 seconds for usability
+- the maximum timeout SHOULD be 300 seconds for security
+
+#### Choice of server
+
+Ultimately it will be up to the Matrix client implementation to decide which rendezvous server to use.
+
+However, it is suggested that the following logic is used by the device/client to choose the rendezvous server in order
+of preference:
+
+1. If the client is already logged in: try and use the current homeserver.
+1. If the client is not logged in and it is known which homeserver the user wants to connect to: try and use that homeserver.
+1. Otherwise use a default server.
+
+### Threat analysis
+
+#### Denial of Service attack surface
+
+Because the rendezvous session protocol allows for the creation of arbitrary channels and storage of arbitrary data, it
+is possible to use it as a denial of service attack surface.
+
+As such, the following standard mitigations such as the following may be deemed appropriate by homeserver
+implementations and administrators:
+
+- rate limiting of requests
+- limiting the number of concurrent sessions
+
+Furthermore, this proposal limits the maximum payload size to 4KB.
+
+#### Data exfiltration
+
+Because the rendezvous session protocol allows for the storage of arbitrary data, it
+is possible to use it to circumvent firewalls and other network security measures.
+
+Implementation may want to block their production IP addresses from being able to make requests to the rendezvous
+endpoints in order to avoid attackers using it as a dead-drop for exfiltrating data.
+
+#### Unsafe content
+
+Because the rendezvous session is not authenticated, it is possible for an attacker to use it to distribute malicious
+content.
+
+This could lead to a reputational problem for the homeserver domain or IPs, as well as potentially causing harm to users.
+
+Mitigations that are included in this proposal:
+
+- the low maximum payload size (4KB)
+- payload is restricted to string
+- the rendezvous session should be short-lived
+- use of `Sec-Fetch-*` headers to not return payload content when browser has navigated to the session URL
+
+## QR code format
+
+To get a good trade-off between visual compactness and high level of error correction we use a binary mode QR with a
+similar structure to that of the existing Device Verification QR code encoding described in [Client-Server
+API](https://spec.matrix.org/v1.9/client-server-api/#qr-code-format).
+
+It is proposed that the QR code format that is currently used in the Client-Server API for
+[device verification](https://spec.matrix.org/v1.16/client-server-api/#qr-code-format) be extended to be more general
+purpose and accommodate this new use case, and future use cases.
+
+The "QR code version" would be repurposed to be a "QR code type" and used as the way to distinguish the format of the
+subsequent data.
+
+The existing cross verification code would be type `0x02`. I suspect that type `0x01` and `0x00` might correspond to
+earlier iterations of the cross signing flow and so might want to be "reserved".
+
+This proposal then adds a new type `0x03`.
+
+The QR codes to be displayed and scanned using this format will encode binary strings in the general form:
+
+- the ASCII string `MATRIX`
+- one byte indicating the QR code type: `0x03` which identifies that the QR is part of this proposal
+- one byte indicating the intent of the device generating the QR:
+ - `0x00` a new device wishing to log in and self-verify
+ - `0x01` an existing device wishing to facilitate the login of a new device and self-verify that other device
+- the ephemeral Curve25519 public key that will be used for [secure channel establishment](#establishment), as 32 bytes
+- the rendezvous session ID encoded as:
+ - two bytes in network byte order (big-endian) indicating the length in bytes of the rendezvous session ID as a UTF-8
+ string
+ - the rendezvous session ID as a UTF-8 string
+- the [base URL] of the homeserver for client-server connections encoded as:
+ - two bytes in network byte order (big-endian) indicating the length in bytes of the base URL as a UTF-8 string
+ (n.b. a base URL longer than 65,535 bytes cannot be encoded and should be rejected)
+ - the base URL as a UTF-8 string
+
+If a new version of this QR sign in capability is needed in future (perhaps with updated secure channel protocol) then
+an additional type can then be allocated which would clearly distinguish this later version.
+
+### Example for QR code generated on new device
+
+A full example for a new device using ephemeral public key `2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws` (base64
+encoded) at rendezvous session ID `e8da6355-550b-4a32-a193-1619d9830668` on homeserver with base URL
+`https://matrix-client.matrix.org` is as follows:
+(Whitespace is for readability only)
+
+```
+4D 41 54 52 49 58
+03 00
+d8 86 68 6a b2 19 7b 78 0e 30 0a 9d 4a 21 47 48 07 00 d7 92 9f 39 ab 31 b9 e5 14 37 02 48 ed 6b
+00 24
+65 38 64 61 36 33 35 35 2D 35 35 30 62 2D 34 61 33 32 2D 61 31 39 33 2D 31 36 31 39 64 39 38 33 30 36 36 38
+00 20
+68 74 74 70 73 3A 2F 2F 6D 61 74 72 69 78 2D 63 6C 69 65 6E 74 2E 6d 61 74 72 69 78 2e 6f 72 67
+```
+
+Which looks as follows as a QR with error correction level Q:
+
+
+
+### Example for QR code generated on existing device
+
+A full example for an existing device using ephemeral public key `2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws` (base64
+encoded), at rendezvous session ID `e8da6355-550b-4a32-a193-1619d9830668` on homeserver with base URL
+`https://matrix-client.matrix.org` is as follows: (Whitespace is for readability only)
+
+```
+4D 41 54 52 49 58
+03 01
+d8 86 68 6a b2 19 7b 78 0e 30 0a 9d 4a 21 47 48 07 00 d7 92 9f 39 ab 31 b9 e5 14 37 02 48 ed 6b
+00 24
+65 38 64 61 36 33 35 35 2D 35 35 30 62 2D 34 61 33 32 2D 61 31 39 33 2D 31 36 31 39 64 39 38 33 30 36 36 38
+00 20
+68 74 74 70 73 3A 2F 2F 6D 61 74 72 69 78 2D 63 6C 69 65 6E 74 2E 6d 61 74 72 69 78 2e 6f 72 67
+```
+
+Which looks as follows as a QR with error correction level Q:
+
+
+
+## Secure channel
+
+The above rendezvous session is insecure, providing no confidentiality nor authenticity against the rendezvous server or
+even arbitrary network participants which possess the rendezvous session ID and server base URL.
+To provide a secure channel on top of this insecure rendezvous session transport, we propose the following scheme.
+
+We use [HPKE](https://www.rfc-editor.org/rfc/rfc9180) in [base
+mode](https://www.rfc-editor.org/rfc/rfc9180.html#name-hpke-modes) instantiated with X25519, HKDF-SHA256 for the KDF and
+ChaCha20-Poly1305 (as specified by [RFC8439](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8)) for the
+authenticated encryption.
+
+The primary limitation of HPKE in base mode is that there is no authentication for the initiating party (the one to send
+the first payload; Device S in the text below). Thus the recipient party (the one to receive the first payload; Device G
+in the text below) has no assurance as to who actually sent the payload. In QR code login, we work around this problem
+by exploiting the fact that both of these devices are physically present during the exchange and offloading the check
+that they are both in the correct state to the user performing the QR code login process.
+
+Additionally the HPKE RFC exclusively defines unidirectional encryption. So, we use the scheme found in the [Oblivious
+HTTP](https://www.rfc-editor.org/rfc/rfc9458) RFC under the [Encapsulation of
+Responses](https://www.rfc-editor.org/rfc/rfc9458#name-encapsulation-of-responses) section to enable
+bidirectional encryption.
+
+We bind the secure channel to the specific rendezvous session by including the homeserver base URL, rendezvous session
+ID and sequence token as additional authentication data in calls to the HPKE `Seal()` and `Open()` functions.
+
+The existing security analyses of HPKE are applicable to our usage. We also discuss some potential pitfalls and attacks.
+
+### Establishment
+
+Participants:
+
+- Device G: the device generating the QR code
+- Device S: the device scanning the QR code
+
+Regardless of which device generates the QR code, either device can be the existing (already signed in) device. The
+other device is then the new device (one seeking to be signed in).
+
+1. **Ephemeral key pair generation**
+
+ Both devices generate an _ephemeral_ Curve25519 key pair:
+
+- Device G generates **(Gp, Gs)**, where **Gp** is its public key and **Gs** the private (secret) key.
+- Device S generates **(Sp, Ss)**, where **Sp** is its public key and **Ss** the private (secret) key.
+
+2. **Create rendezvous session**
+
+Device G creates a rendezvous session by making a `POST` request (as described previously) to the nominated homeserver
+with an empty payload. It parses the **ID** received and **sequence token**.
+
+3. **Initial key exchange**
+
+Device G displays a QR code containing sufficient information for the scanning device to locate the rendezvous session
+and establish the secure channel.
+
+The information to be encoded is:
+
+- Its public key **Gp**
+- The insecure rendezvous session **ID**
+- An indicator (the **intent**) to say if this is the new device which wishes to login, or an existing device
+that wishes to facilitate the login of the new device
+- the Matrix homeserver **[base URL]**
+
+The format of this QR is defined in detail in a [separate section](#qr-code-format) of this proposal.
+
+Device S scans and parses the QR code to obtain **Gp**, the rendezvous session **ID**, **intent** and the Matrix homeserver
+**[base URL]**.
+
+At this point Device S should check that the received intent matches what the user has asked to do on the device.
+
+4. **Device S sends the initial payload**
+
+Device S performs an ECDH operation using **Ss** and **Gp** to compute the shared secret **SharedSecret**.
+
+The **SharedSecret** is then used to initialize an
+[HPKE encryption context](https://www.rfc-editor.org/rfc/rfc9180.html#name-encryption-and-decryption)
+**Context_DeviceS_Send** using the `KeySchedule()` function.
+
+After this step, **Ss** is discarded.
+
+**Context_DeviceS_Send**
+
+```
+SharedSecret := ECDH(Ss, Gp)
+
+Context_DeviceS_Send := KeySchedule(mode=0x00, shared_secret=SharedSecret, info="MATRIX_QR_CODE_LOGIN")
+```
+
+With this, Device S has established its sending side of the secure channel. Device S then derives a confirmation payload
+that Device G can use to confirm that the channel is secure. It contains:
+
+- The string `MATRIX_QR_CODE_LOGIN_INITIATE`, encrypted and authenticated with ChaCha20-Poly1305 using
+ the `ContextS.Seal()` function of context **Context_DeviceS_Send** with additional authentication data:
+ - the homeserver **base URL** from the QR code
+ - the rendezvous session **ID** from the QR code
+ - the **sequence token** returned by the homeserver when calling `GET` on the rendezvous session
+- Its public ephemeral key **Sp**.
+
+```
+Aad := EncodeStringAsBytes(BaseUrl) || EncodeStringAsBytes(RendezvousId) || EncodeStringAsBytes(SequenceToken)
+TaggedCiphertext := Context_DeviceS_Send.Seal("MATRIX_QR_CODE_LOGIN_INITIATE", Aad)
+LoginInitiateMessage := UnpaddedBase64(Sp || TaggedCiphertext)
+```
+
+We define the result of `EncodeStringAsBytes(StringInput)` to be a sequence of bytes:
+
+- two bytes in network byte order (big-endian) indicating the length in bytes of the `StringInput` as a UTF-8 string
+- the `StringInput` as a UTF-8 string
+
+e.g. `EncodeStringAsBytes("abcdef")` returns `[0x00, 0x06, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66]`
+
+n.b. Because this proposal restricts the length of `RendezvousId` and `SequenceToken` to 65535 bytes, and that a
+`BaseUrl` longer than 65535 bytes will have failed at the point of encoding a QR, we don't specify a handling for
+`StringInput` of length greater than 65535 bytes.
+
+Device S then sends the **LoginInitiateMessage** as the `data` payload to the rendezvous session using a `PUT` request
+and noting the new **sequence token**.
+
+5. **Device G confirms**
+
+Device G receives **LoginInitiateMessage** (potentially coming from Device S) from the insecure rendezvous session by
+polling with `GET` requests.
+
+It then does the reverse of the previous step, obtaining **Sp**, deriving the shared secret using ECDH on **Gs** and **Sp**,
+discarding **Gs**, deriving an HPKE context `Context_DeviceG_Receive`:
+
+**Context_DeviceG_Receive**
+
+```
+(TaggedCiphertext, Sp) := Unpack(LoginInitiateMessage)
+
+SharedSecret := ECDH(Gs, Sp)
+
+Context_DeviceG_Receive := KeySchedule(mode=0x00, shared_secret=SharedSecret, info="MATRIX_QR_CODE_LOGIN")
+```
+
+It then decrypts (and authenticates) the message using the `ContextR.Open()` function of **Context_DeviceG_Receive**
+with the additional authentication data:
+- the homeserver **base URL** as before
+- the rendezvous session **ID** as before
+- the **sequence token** returned by the homeserver when the original `POST` request was made to the rendezvous session
+
+It checks that the plaintext matches the string `MATRIX_QR_CODE_LOGIN_INITIATE`, failing and aborting if not.
+
+```
+Aad := EncodeStringAsBytes(BaseUrl) || EncodeStringAsBytes(RendezvousId) || EncodeStringAsBytes(SequenceToken)
+Plaintext := Context_DeviceG_Receive.Open(TaggedCiphertext, Aad)
+
+unless Plaintext == "MATRIX_QR_CODE_LOGIN_INITIATE":
+ FAIL
+```
+
+It then derives its own HPKE context **Context_DeviceG_Send** for sending based on the scheme from the
+[Oblivious HTTP](https://www.rfc-editor.org/rfc/rfc9458#name-encapsulation-of-responses) RFC, using a secret exported
+from **Context_DeviceG_Receive**:
+
+**Context_DeviceG_Send**
+
+```
+Secret := Context_DeviceG_Receive.Export("MATRIX_QR_CODE_LOGIN response", 32)
+
+ResponseNonce := random(32)
+Salt := Sp || ResponseNonce
+
+AeadKey := HKDF_SHA256(Secret, "key", salt=Salt, size=32)
+AeadNonce := HKDF_SHA256(Secret, "key", salt=Salt, size=12)
+ExporterSecret := [0; 32]
+
+Context_DeviceG_Send := Context(AeadKey, AeadNonce, 0, ExporterSecret)
+```
+
+**Warning** The exporter interface of the **Context_DeviceG_Send** context **MUST NOT** be used, as it is initialized
+with a dummy exporter secret. Aside from this limitation, the response context supports encryption and decryption in the
+same manner as the primary context, enabling bidirectional use of HPKE.
+
+Following the creation of the response context **Context_DeviceG_Send**, it responds with a dummy payload containing the
+string `MATRIX_QR_CODE_LOGIN_OK` that is sealed with the additional authentication data similar to before, but the
+**sequence token** is the one that was received with the `GET` request that returned **LoginInitiateMessage**:
+
+```
+Aad := EncodeStringAsBytes(BaseUrl) || EncodeStringAsBytes(RendezvousId) || EncodeStringAsBytes(SequenceToken)
+TaggedCiphertext := Context_DeviceG_Send.Seal("MATRIX_QR_CODE_LOGIN_OK", Aad)
+LoginOkMessage := UnpaddedBase64Encode(ResponseNonce || TaggedCiphertext)
+```
+
+Device G sends **LoginOkMessage** as the `data` payload via a `PUT` request to the insecure rendezvous session.
+
+6. **Verification by Device S**
+
+> [!TIP]
+> _A helpful [note](https://github.com/matrix-org/matrix-spec-proposals/pull/4388/changes#r3025116401) from @uhoreg to
+> future readers:_
+>
+> The security of this scheme is established by Device S decrypting and verifying the **LoginOkMessage** message as
+> described in this step. The messages are encrypted by the result of the ECDH of G and S's ephemeral keys (plus extra
+> steps). S knows that it has G's public key because it was obtained from the QR scan (we assume that the attacker can't
+> modify the displayed QR code). So if it is able to decrypt and verify the message, then it knows:
+>
+> - that it was received directly from G, since nobody other than S and G could obtain the shared secret to encrypt
+> the message. In other words, the message from G to S was not tampered with.
+> - that G has received the correct public key from S, otherwise the message from G would have been encrypted using
+> the wrong key, and would be undecryptable/unverifiable.
+>
+> Since, at this point, S knows that both devices have the correct public key for the other device, it knows that the
+> encrypted channel is secure.
+
+Device S receives a response over the insecure rendezvous session by polling with `GET` requests, potentially from
+Device G.
+
+It proceeds to derive the response context by unpacking the base response nonce from the **LoginOkMessage** and
+creating a response context on its own.
+
+**Context_DeviceS_Receive**
+
+```
+(TaggedCiphertext, ResponseNonce) := Unpack(LoginOkMessage)
+
+Secret := Context_DeviceS_Send.Export("MATRIX_QR_CODE_LOGIN response", 32)
+Salt := Sp || ResponseNonce
+
+AeadKey_G := HKDF_SHA256(Secret, "key", salt=Salt, size=32)
+AeadNonce := HKDF_SHA256(Secret, "nonce", salt=Salt, size=12)
+ExporterSecret := [0; 32]
+
+Context_DeviceS_Receive := Context(AeadKey, AeadNonce, 0, ExporterSecret)
+```
+
+It decrypts (and authenticates) the response using the **Context_DeviceS_Receive** context, which will succeed provided
+the payload was indeed sent by Device G. The additional authentication data is as before with the **sequence token**
+being the one that was received when the `PUT` was made for **LoginInitiateMessage**.
+It then verifies the plaintext matches `MATRIX_QR_CODE_LOGIN_OK`, failing
+otherwise.
+
+```
+Aad := EncodeStringAsBytes(BaseUrl) || EncodeStringAsBytes(RendezvousId) || EncodeStringAsBytes(SequenceToken)
+Plaintext := Context_DeviceS_Receive.Open(TaggedCiphertext, Aad)
+
+unless Plaintext == "MATRIX_QR_CODE_LOGIN_OK":
+ FAIL
+```
+
+If the above was successful, Device S then calculates a two digit **CheckCode** code using the [HPKE export
+interface](https://www.rfc-editor.org/rfc/rfc9180.html#hpke-export) of context **Context_DeviceS_Send**. **Gp** and
+**Sp** are used as inputs for the export interface:
+
+```
+CheckBytes := Context_DeviceS_Send.Export("MATRIX_QR_CODE_LOGIN_CHECKCODE" || Gp || Sp , size=2)
+CheckCode := NumToString((CheckBytes[0] % 9) + 1) || NumToString(CheckBytes[1] % 10)
+```
+
+Device S then displays an indicator to the user that the secure channel has been established and that the **CheckCode**
+should be entered on the other device when prompted. Example wording could say "Secure connection established. Enter the
+code XY on your other device."
+
+7. **Out-of-band confirmation**
+
+**Warning**: *This step is crucial for the security of the scheme since it overcomes the aforementioned limitation of
+HPKE.*
+
+Device G asks the user to enter the **CheckCode** that is being displayed on Device S.
+
+The purpose of the code being entered is to ensure that the user has actually checked their other device rather than
+just pressing "continue", and that the Device S has been able to determine that the channel is secure.
+
+Because the actual value of the code is not significant from a cryptographic point of view it is acceptable that the
+digits 6, 7, 8 and 9 are slightly less likely to appear. Furthermore we also ensure that the first digit of the code is
+not `0` to avoid confusion the user might have about whether to enter a leading zero.
+
+The exact points in the flow that the user is prompted for the **CheckCode** is described in [MSC4108].
+
+Device G compares the code that the user has entered with the **CheckCode** that it calculates using the same mechanism
+as before:
+
+```
+CheckBytes := Context_DeviceG_Receive.Export("MATRIX_QR_CODE_LOGIN_CHECKCODE" || Gp || Sp , size=2)
+CheckCode := NumToString((CheckBytes[0] % 9) + 1) || NumToString(CheckBytes[1] % 10)
+```
+
+If the code that the user enters matches then the secure channel is established.
+
+Subsequent payloads sent from G should be encrypted using the context **Context_DeviceG_Send**, while payloads
+sent from S should be encrypted with **Context_DeviceS_Send**. Each call to the `Seal()` function should use the
+additional authentication data of the form where the **sequence token** is from the last `GET` that the device received:
+
+```
+Aad := EncodeStringAsBytes(BaseUrl) || EncodeStringAsBytes(RendezvousId) || EncodeStringAsBytes(SequenceToken)
+```
+
+Similarly, payloads received by G should be decrypted using the context **Context_DeviceG_Receive**, while payloads received by S
+should be decrypted using the context **Context_DeviceG_Receive**. Each call to the `Open()` function should use the
+additional authentication data of the form where the **sequence token** is from the last `PUT` that the device made:
+
+```
+Aad := EncodeStringAsBytes(BaseUrl) || EncodeStringAsBytes(RendezvousId) || EncodeStringAsBytes(SequenceToken)
+```
+
+### Sequence diagram
+
+The sequence diagram for the secure channel establishment is as follows:
+
+```mermaid
+sequenceDiagram
+ participant G as Device G
+ participant Z as Homeserver
+ participant S as Device S
+
+ note over G,S: 1) Devices G and S each generate an ephemeral Curve25519 key pair
+
+ activate G
+ note over G: 2) Device G creates a rendezvous session as follows
+ G->>+Z: POST /_matrix/client/v1/rendezvous
{"data": ""}
+ Z->>-G: 200 OK
{"id": "abc-def", "sequence_token": "1", "expires_in_ms": 300000}
+
+ note over G: 3) Device G generates and displays a QR code containing:
its ephemeral public key, the rendezvous session ID, the server base URL
+
+ G-->>S: Device S scans the QR code shown by Device G
+ deactivate G
+
+ activate S
+ note over S: Device S validates QR scanned and the rendezvous session ID
+
+ S->>+Z: GET /_matrix/client/v1/rendezvous/abc-def
+ Z->>-S: 200 OK
{"sequence_token": "SEQ1", "expires_in_ms": 300000, "data": ""}
+
+ note over S: 4) Device S creates context Context_DeviceS_Send and LoginInitiateMessage (sealed using sequence token SEQ1).
It sends LoginInitiateMessage via the rendezvous session
+ S->>+Z: PUT /_matrix/client/v1/rendezvous/abc-def
{"sequence_token": "SEQ1", "data": ""}
+ Z->>-S: 200 OK
{"sequence_token": "SEQ2"}
+ deactivate S
+
+ G->>+Z: GET /_matrix/client/v1/rendezvous/abc-def
+ activate G
+ Z->>-G: 200 OK
{"sequence_token": "SEQ2", "expires_in_ms": 300000, "data": ""}
+ note over G: 5) Device G attempts to parse Data as LoginInitiateMessage after creating Context_DeviceG_Receive
+ note over G: Device G checks that the plaintext (unsealed using sequence token SEQ1) matches MATRIX_QR_CODE_LOGIN_INITIATE
+
+ note over G: Device G creates Context_DeviceG_Send
+ note over G: Device G computes LoginOkMessage (sealed using sequence token SEQ2) and sends to the rendezvous session
+
+ G->>+Z: PUT /_matrix/client/v1/rendezvous/abc-def
{"sequence_token": "SEQ2", "data": ""}
+ Z->>-G: 200 OK
{"sequence_token": "SEQ3"}
+ deactivate G
+
+ activate S
+ S->>+Z: GET /_matrix/client/v1/rendezvous/abc-def
+ Z->>-S: 200 OK
{"sequence_token": "SEQ3", "expires_in_ms": 300000, "data": ""}
+
+ note over S: 6) Device S attempts to parse Data as LoginOkMessage
+ note over S: 6) Device S creates Context_DeviceS_Receive
+ note over S: Device S checks that the plaintext (unsealed using sequence token SEQ2) matches MATRIX_QR_CODE_LOGIN_OK
+
+ note over S: If okay, Device S calculates the CheckCode to be displayed
+ note over S: Device S displays a green checkmark, "secure connection established" and the CheckCode
+ note over S: Device S knows that the channel is secure
+ deactivate S
+
+ note over G: 7) Device G asks the user to confirm that the other device is showing a green checkmark and enter the CheckCode
+ note over G: If the user enters the correct CheckCode and confirms that a green checkmark is shown then Device G knows that the channel is secure
+```
+
+### Secure operations
+
+Conceptually, once established, the secure channel offers two operations, `SecureSend` and `SecureReceive`, which wrap
+the `Send` and `Receive` operations offered by the rendezvous session API to securely send and receive data between two devices.
+
+### Threat analysis
+
+In an attack scenario, we add a participant called Specter with the following capabilities:
+
+- Specter is present for QR code generation/scanning ("shoulder-surfing") and can scan the code themselves.
+- Specter has full control over the network (in a Dolev-Yao sense), being able to observe and modify all traffic.
+- Specter controls both the homeserver and the rendezvous server.
+
+#### Replay protection
+
+Due to use of ephemeral key pairs which are immediately discarded after use, each QR code login session derives a unique
+secret so payloads from earlier sessions cannot be replayed. Each payload in the session is unique and expected only
+once. Finally, replay of the initial message from S to G is prevented by the use of a randomly generated base nonce in
+the response from G to S, while deterministic per-message nonces prevent replay of any subsequent messages.
+
+#### Pure Dolev-Yao attacker
+
+An attacker with control over the network but _not_ present for the QR code scanning cannot thwart the process since
+they are unable to obtain the ephemeral key **Gp** of Device G.
+
+#### Shoulder-surfing attacker (Specter)
+
+Since Device G has no way of authenticating Device S, an attacker present for the QR code scanning can learn **Gp** and
+attempt to mimic Device S in order to get their Device S signed in instead.
+
+- In step 3, Specter can shoulder surf the QR code scanning to obtain **Gp**.
+- In step 4, Specter can intercept S's payload and replace it with a payload of their own, replacing **Sp** with its
+own key.
+- The attack is only thwarted in step 7, because Device S won't ever display the indicator of success to the user. The
+user then must cancel the process on Device G, preventing it from sharing any sensitive material.
+
+### Choice of message prefix
+
+During the secure channel establishment the messages have been prefixed with `MATRIX_QR_CODE_LOGIN_` rather than
+something more generic. The purpose is to bind the protocol to this specific application.
+
+Whilst there could be other uses for the secure channel mechanism or we might establish communication between devices
+using another mechanism (e.g. NFC or sound), this proposal only considers the scenario where the communication is
+initiated via QR code and we make the prefix explicitly named to match.
+
+## Potential issues
+
+Because this is an entirely new set of functionality it should not cause issue with any existing Matrix functions or capabilities.
+
+The proposed protocol requires the devices to have IP connectivity to the server which might not be the case in P2P scenarios.
+
+## Alternatives
+
+### Alternative to the rendezvous session protocol
+
+#### ETag based rendezvous API
+
+An earlier iteration of this MSC used an alternative rendezvous API that was based on
+[MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886).
+
+However, it was found to have issues including:
+
+- the ETags were getting mangled by proxies and load balancers
+- the semantics of the API are different from the rest of the Matrix Client-Server API
+- the CORS header changes required additional configuration work
+
+The present iteration of the rendezvous API described in this MSC attempts to "feel" more like a Matrix Client-Server
+API.
+
+#### Send-to-Device messaging
+
+If you squint then this proposal looks similar in some regards to the existing
+[Send-to-device messaging](https://spec.matrix.org/v1.9/client-server-api/#send-to-device-messaging) capability.
+
+Whilst to-device messaging already provides a mechanism for secure communication between two Matrix clients/devices, a
+key consideration for the anticipated login with QR capability is that one of the clients is not yet authenticated with
+a homeserver.
+
+Furthermore the client might not know which homeserver the user wishes to connect to.
+
+Conceptually, one could create a new type of "guest" login that would allow the unauthenticated client to connect to a
+homeserver for the purposes of communicating with an existing authenticated client via to-device messages.
+
+Some considerations for this:
+
+Where the "actual" homeserver is not known then the "guest" homeserver nominated by the new client would need to be
+federated with the "actual" homeserver.
+
+The "guest" homeserver would probably want to automatically clean up the "guest" accounts after a short period of time.
+
+The "actual" homeserver operator might not want to open up full "guest" access so a second type of "guest" account might
+be required.
+
+Does the new device/client need to accept the T&Cs of the "guest" homeserver?
+
+#### Other existing protocols
+
+One could try and do something with STUN or TURN or [COAP](https://datatracker.ietf.org/doc/html/rfc7252).
+
+#### Implementation details
+
+Rather than requiring the devices to poll for updates, "long-polling" could be used instead similar to `/sync`. Or WebSockets.
+
+#### Unauthenticated device could create a "redirect channel" without payload
+
+In the current proposal the server operator may choose to not allow unauthenticated devices to create a rendezvous
+session to reduce abuse/attack vectors.
+
+In this scenario it means that the unauthenticated client cannot create the QR code.
+
+An alternative would be to do something like this:
+
+1. Unauthenticated device (UD) creates a "redirect channel" on HS1 and sets that in the QR code.
+1. The authenticated device (AD) creates a rendezvous channel on HS2.
+1. HS2 POSTS to the redirect channel on HS1 with the homeserver and rendezvous channel ID. HS1 validates its from HS2.
+1. HS1 returns the homeserver (HS2) and rendezvous channel ID to UD, who then uses that channel as normal.
+
+This has the following properties:
+
+1. It limits how much information can be persisted on an unauthenticated channel. We can severely restrict the size
+ of the request ID for example.
+1. An abuser must use a domain they own if they want to encode dodgy data in the rendezvous channel ID. We can then ban
+ abusive domains.
+1. An unauthenticated device can only receive information, rather than create a 2-way channel. Not sure that's at all
+ useful thing to assert, but it is nonetheless a property.
+1. For each redirect channel created, you can only send one payload. This makes it easier to heavily ratelimit.
+
+Erik [said](https://github.com/matrix-org/matrix-spec-proposals/pull/4108#discussion_r2336295451):
+> I think this sort of flow would reduce potential abuse vectors, but equally makes things more complicated and may not
+> be worth it.
+
+#### Define the Sec-Fetch unsafe content logic
+
+We could explicitly define exactly which `Sec-Fetch-*` headers are to be checked and which values disallowed.
+
+It could also be made a MUST rather than SHOULD.
+
+However, it has been deliberately left as an implementation detail such that alternative protections can be used if
+appropriate for a particular deployment scenario, or if a better mechanism becomes available.
+
+#### Initialise `Context_DeviceG_Send` with a real exporter secret
+
+It has been suggested that the "warning" about not using the exporter interface of `Context_DeviceG_Send` could be
+removed.
+
+To do so we could instead initialize `Context_DeviceG_Send`` with some real exporter secret, but that would be more
+expensive and we wouldn't use the exporter interface of the response context.
+
+Another alternative would be to not use the HPKE context for the other direction, but then we would need to reimplement
+the nonce handling.
+
+### Alternative QR code formats
+
+An earlier version of this proposal kept the "version" byte at `0x02` and added additional "mode"
+values of `0x03` (which is now intent `0x00`) and `0x04` (which is now intent `0x01`).
+
+The current usage of converting the "version" to be a "type" _feels_ like a more intuitive use of
+the bytespace.
+
+Another alternative was to use a human readable prefix such as `MATRIX_LOGIN` instead of `MATRIX`.
+This was discounted on the basis of wanting to keep the QR reasonably compact.
+
+## Security considerations
+
+This proposed mechanism has been designed to protects users and their devices from the following threats:
+
+- A malicious actor who is able to scan the QR code generated by the legitimate user.
+- A malicious actor who can intercept and modify traffic on the application layer, even if protected by encryption like TLS.
+- Both of the above at the same time.
+
+Additionally, the homeserver is able to define and enforce policies that can prevent a sign in on a new device.
+Such policies depend on the homeserver in use and could include, but are not limited to, time of day, day of the week,
+source IP address and geolocation.
+
+A threat analysis has been done within each of the key layers in the proposal above.
+
+## Unstable prefix
+
+### Rendezvous API prefix
+
+While this feature is in development the new API endpoints should be exposed using the following unstable prefix:
+
+- `/_matrix/client/unstable/io.element.msc4388/rendezvous` instead of `/_matrix/client/v1/rendezvous`
+
+### Unstable QR code format
+
+The unstable value of `IO_ELEMENT_MSC4388` should be used instead of `MATRIX` in the QR code.
+
+A full example for an existing device using ephemeral public key `2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws` (base64
+encoded), at rendezvous session ID `e8da6355-550b-4a32-a193-1619d9830668` on homeserver `https://matrix-client.matrix.org` is as follows: (Whitespace is for readability only)
+
+```
+49 4F 5F 45 4C 45 4D 45 4E 54 5F 4D 53 43 34 33 38 38
+03 01
+d8 86 68 6a b2 19 7b 78 0e 30 0a 9d 4a 21 47 48 07 00 d7 92 9f 39 ab 31 b9 e5 14 37 02 48 ed 6b
+00 24
+65 38 64 61 36 33 35 35 2D 35 35 30 62 2D 34 61 33 32 2D 61 31 39 33 2D 31 36 31 39 64 39 38 33 30 36 36 38
+00 20
+68 74 74 70 73 3A 2F 2F 6D 61 74 72 69 78 2D 63 6C 69 65 6E 74 2E 6d 61 74 72 69 78 2e 6f 72 67
+```
+
+Which looks as follows as a QR with error correction level Q:
+
+
+
+It is suggested that this unstable QR prefix convention could be used by future proposals.
+
+### M_CONCURRENT_WRITE errcode
+
+The unstable value of `IO_ELEMENT_MSC4388_CONCURRENT_WRITE` should be used instead of `M_CONCURRENT_WRITE`.
+
+## Dependencies
+
+None.
+
+[base URL]: https://spec.matrix.org/v1.16/client-server-api/#getwell-knownmatrixclient
+[MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
diff --git a/proposals/images/4388-qr-intent00.png b/proposals/images/4388-qr-intent00.png
new file mode 100644
index 00000000000..9cef72ef3ab
Binary files /dev/null and b/proposals/images/4388-qr-intent00.png differ
diff --git a/proposals/images/4388-qr-intent01-unstable.png b/proposals/images/4388-qr-intent01-unstable.png
new file mode 100644
index 00000000000..30ecd14c701
Binary files /dev/null and b/proposals/images/4388-qr-intent01-unstable.png differ
diff --git a/proposals/images/4388-qr-intent01.png b/proposals/images/4388-qr-intent01.png
new file mode 100644
index 00000000000..e1111422048
Binary files /dev/null and b/proposals/images/4388-qr-intent01.png differ