diff --git a/proposals/3906-sign-in-with-qr.md b/proposals/3906-sign-in-with-qr.md new file mode 100644 index 00000000000..3c845abf6cd --- /dev/null +++ b/proposals/3906-sign-in-with-qr.md @@ -0,0 +1,564 @@ +# MSC3906: Protocol to use an existing Matrix client session to complete login and setup of E2EE via QR code + +This MSC proposes a method to allow an existing Matrix authenticated +client/device to sign in a new device and handle E2EE set up (mutual device +verification; setup of cross-signing if used; connecting to room key backups if used). + +There are currently two use cases for this proposal: + +- allowing a user to login and setup E2EE on an additional Matrix client by means +of scanning a QR code +- a mechanism to facilitate the launching of advanced E2EE aware "widgets" which +are acting as full Matrix clients + +## Secure channel prerequisite + +This proposal relies on a secure "rendezvous" channel having been established +between the two devices. (see dependencies section) + +The initiation of a secure channel could be via sharing a QR code or be through +some other means. + +By way of example this is what the QR for an X25519 based rendezvous via HTTP +looks like in the Element Web +[prototype](https://pr9303--matrix-react-sdk.netlify.app/) where the code was +generated on the new device: + +```json +{ + "rendezvous": { + "algorithm": "m.rendezvous.v2.curve25519-aes-sha256", + "key": "2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws=", + "transport": { + "type": "http.v1", + "uri": "https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668" + } + }, + "flow": "m.setup.additional_device", + "intent": "login.start" +} +``` + +This proposal should also work with a to_device based transport channel too. + +Furthermore it should be adaptable to work with OIDC based authentication in future. + +The purpose of the `flow` field is to allow for future extensions such as updated +flow semantics or other flows entirely. + +## Proposal + +This proposal defines the `m.setup.additional_device` flow that is to be +used once a secure channel has been established. + +The following `intent` values are defined: + +- `login.start` - a device with this intent is a new device that wishes to be +set up +- `login.reciprocate` - a device with this intent is already set up and is able +to facilitate set up of a new device + +For the intents to be compatible one device must have the `login.start` intent +and the other device must have the `login.reciprocate` intent. + + +The initial steps of the secure channel setup are only included for completeness: + +```mermaid +sequenceDiagram + participant A as New device wanting to sign in + participant B as Existing device already signed in + participant HS as Homeserver +Note over A,B: Devices establish secure rendezvous and derive
confirmation code from shared key. e.g. 1234-5678-9012
The intention of the initiator (generator of the scanned code) was encoded in the scanned code + + alt Code was scanned on new device + rect rgba(240,240,240,0.5) + Note over A,B: These are from MSC3886 + B->>A: Rendezvous set up and ECDH public key encoded in QR + A->>B: New device responds with ECDH public key + end + Note over A: 1. Check compatibility of intents + alt Incompatible + A->>B: 2. {"type":"m.login.failure", "reason": "incompatible_intent", "intent": "login.start"} + B->>B: Cancel rendezvous + end + else Code was scanned on existing device + rect rgba(240,240,240,0.5) + Note over A,B: These are from MSC3886 + A->>B: Rendezvous set up and ECDH public key encoded in QR + B->>A: Existing device responds with ECDH public key + end + Note over B: 1. Check compatibility of intents + alt Incompatible + B->>A: 2. {"type":"m.login.failure", "reason": "incompatible_intent", "intent": "login.reciprocate"} + A->>A: Cancel rendezvous + end + A->>B: 3. {"type":"m.login.progress"} + end + + Note over B: 4. Determine if MSC3882 is available: + B->>+HS: GET /capabilities + HS-->>-B: supported capabilities + alt no protocol available + B->>A: 5. {"type":"m.login.failure", "reason":"unsupported"} + else send available protocols + B->>A: 6. {"type":"m.login.protocols", "protocols":["login_token"]} + end + + Note over A: Check that suitable protocol available + A->>B: 7. {"type":"m.login.protocol", "protocol": "login_token"} + + Note over A,B: 8. Confirmation code should be displayed on both devices e.g 1234-5678-9012 + + Note over B: 9. User asked to approve or deny login + + alt User declined + B->>A: 10. {"type":"m.login.declined"} + else User approved + + B->>+HS: 11. POST /login/get_token + HS-->>-B: {"login_token": "abcdefghijkl", "expires_in_ms": 30000} + + B->>A: 12. {"type": "m.login.approved", "login_token": "abcdefghijkl","homeserver": "https://matrix-client.matrix.org"} + + A->>+HS: 13. POST /login {"type": "m.login.token", "login_token": "abcdefghijkl"} + HS-->>-A: {"access_token": "asdsad", "device_id": "AAABBBCCC"} + + alt no-E2EE + A->>B: 14. {"type": "m.login.success"} + else E2EE + + Note over A: E2EE device keys generated and uploaded to HS as normal + + A->>B: 15. { "type": "m.login.success",
"device_key": "zxcxzcxzc",
"device_id": "AAABBBCCC"} + + loop Timeout of 10 seconds + B->>B: 16. Wait for new device to come online + end + + Note over B: 17. check that device key from homeserver vs received in 15 match + B->>B: 18a. Mark new device (AAABBBCCC) as verified locally + + alt Cross-signing active + B->>B: 18b. Sign new device + B->>HS: 18b. Upload signature + B->>A: 19a. { "type": "m.login.verified",
"verifying_device_id": "XXXYYYZZZ", "verifying_device_key": "abcdef",
"master_key": } + else No cross-signing + B->>A: 19b. { "type": "m.login.verified",
"verifying_device_id": "XXXYYYZZZ", "verifying_device_key": "abcdef" } + end + + Note over A: 20. Check that verifying_device_id and verifying_device_key match data from homeserver. If no match then abort + A->>A: 21. If master_key was received then mark as trusted + alt `master_key` was received + Note over A: 21a. Check that it matches the master key
received from the homeserver. If it does not match then abort the process reporting a security error to the user. + A->>A: 21b. Mark master key as trusted + end + A->>A: 22. Mark existing device (XXXYYYZZZ) as verified locally + + A->>HS: m.secret.requests for MSK/USK/SSK/backup secrets from existing device XXXYYYZZZ + loop + HS->>B: m.secret.request + B->>HS: m.secret.send + HS->>A: m.secret.send + A->>A: Check and store received secret locally + end + end + end +``` + +1. The scanning device determines if the intents are compatible. + +The `intent` is either: + +- `login.start` - the device/client wishes to sign in +- `login.reciprocate` - the device/client wishes to sign in another device + +2. If they are not compatible then the client sends a payload to indicate that +it isn't compatible: + +```json +{ + "type": "m.login.failure", + "reason": "incompatible_intent", + "intent": "" +} +``` + +Both side can then clean up the rendezvous and provide feedback to the user. + +3. In the case that the new device scanned the code an empty progress payload +is sent so that the the existing device knows it can proceed: + +*New device => Existing device:* + +```json +{ + "type": "m.login.progress" +} +``` + +4. The existing device determines if the homeserver has sufficient capabilities +to support the request (i.e. support for MSC3882). To present the best UX it is recommended +that the client checks the capabilities before offering the ability to generate or scan a QR code. + +5. If it doesn't then it responds with the following and closes the rendezvous: + +*Existing device => New device:* + +```json +{ + "type": "m.login.failure", + "reason": "unsupported" +} +``` + +6. Otherwise, the existing device acknowledges the request and indicates the +protocols available: + +*Existing device => New device:* + +```json +{ + "type": "m.login.protocols", + "protocols": ["login_token"] +} +``` + +7. The new device can then choose whether to proceed with a protocol at this +point. If so it sends: + +*New device => Existing device:* + +```json +{ + "type": "m.login.protocol", + "protocol": "login_token" +} +``` + +It could decline with something like: + +*New device => Existing device:* + +```json +{ + "type": "m.login.failure", + "reason": "unsupported" +} +``` + +8. A 12 numerical digit confirmation code derived from the shared key used by +the rendezvous channel must be displayed on both devices. + +9. On the existing device the user must be prompted to approve the new login +along with the confirmation code visible. + +10. If the user declines the request: + +*Existing device => New device:* + +```json +{ + "type": "m.login.declined", +} +``` + +11. The existing device calls `POST /login/get_token` as per MSC3882 to obtain a +`login_token`. n.b. If the homeserver responds with UIA challenge then the +existing device must complete UIA. + +12. The existing device then sends the login token to the new device along with +the homeserver to use it with: + +*Existing device => New device:* + +```json +{ + "type": "m.login.approved", + "login_token": "abcdefghijkl", + "homeserver": "https://matrix-client.matrix.org" +} +``` + +13. New device calls `POST /login` to redeem the `login_token`. +e.g. `{ "type": "m.login.token", "token": }` + +New device informs existing device of outcome and if E2EE is to be setup then +includes device ID and keys: + +14. No E2EE: + +*New device => Existing device:* + +```json +{ + "type": "m.login.success", +} +``` + +15. With E2EE: + +*New device => Existing device:* + +```json +{ + "type": "m.login.success", + "device_id": "AAABBBCCC", + "device_key": "zxcxzcxzc" +} +``` + +Fields: + +- `device_id` is the device ID of the new device. +- `device_key` is the base64 encoded Ed25519 key of the new device. + +16. If doing E2EE then existing device then waits for up to 10 seconds for the +`device_id` to become visible. + +17. If the device is visible within the time period then the existing device +must first check that the `device_id` and `device_key` match those provided by +the homeserver. If they do not match then abort the process reporting a security +error to the user. + +18a. Mark the new device as locally verified on existing device + +18b. If cross-signing is in use then cross-sign the new device and upload +signature to homeserver. + +19. The existing device notified the new device that verification has been +completed on its end: + +*Existing device => New device:* + +```json + "type": "m.login.verified", + "verifying_device_id": "XXXYYYZZZ", + "verifying_device_key": "abcdefgh" +``` + +If cross-signing is in use then the public part of the master signing key +`master_key` should be included: + +*Existing device => New device:* + +```json + "type": "m.login.verified", + "verifying_device_id": "XXXYYYZZZ", + "verifying_device_key": "abcdefgh", + "master_key": "mmmmmmmm" +``` + +Fields: + +- `verifying_device_id` is the device ID for the existing device. +- `verifying_device_key` is the base64 encoded Ed25519 key of the existing device. +- `master_key` is the base64 encoded public part of the master signing key. + +20. The new device checks that `verifying_device_id` and `verifying_device_key` +match those received from the homeserver. If they do not match then abort the +process reporting a security error to the user. + +21a. If the `master_key` was received then check that it matches the master key +received from the homeserver. If it does not match then abort the process +reporting a security error to the user. + +21b. If it matched the new device marks the `master_key` as locally trusted. + +22. Only after the above security checks, only now should the new device mark +the verifying device as locally verified. + +23. The new device requests that the verifying device share the +`m.cross_signing.master`, `m.cross_signing.user_signing`, +`m.cross_signing.self_signing`, `m.megolm_backup.v1` secrets by sending +[`m.secret.request` device events](https://spec.matrix.org/v1.6/client-server-api/#sharing) + +### Anticipated errors + +- Neither device is already signed in +- Both devices are already signed in + +### Cancellation + +The new device could cancel the process at any time by sending: + +*New device => Existing device:* + +```json +{ + "type": "m.login.failure", + "reason": "cancelled" +} +``` + +### OIDC variant + +It is anticipated that a variant of this would work with OIDC in future +identified by a different `protocol` value. + +To be clear, it is not proposed that a OIDC protocol is defined at this time, +but instead this is included here to help get comfortable that this proposal is +reasonably future proof. + +Whilst not fully thought through it could work like this: + +```mermaid +sequenceDiagram + participant A as New device
wanting to sign in + participant B as Existing device
already signed in + participant OP as OIDC Provider + participant HS as + rect rgba(240,240,240,0.5) + Note over A,B: These steps are same as login.token flow + alt Code was scanned on new device + B->>A: Rendezvous set up and ECDH public key encoded in QR + A->>B: New device responds with ECDH public key + Note over B: 1. Check compatibility of intents + alt Incompatible + B->>A: 2. {"type":"m.login.failure", "reason": "incompatible_intent", "intent": "login.start"} + A->>A: Cancel rendezvous + end + else Code was scanned on existing device + A->>B: Rendezvous set up and ECDH public key encoded in QR + B->>A: New device responds with ECDH public key + Note over A: 1. Check compatibility of intents + alt Incompatible + A->>B: 2. {"type":"m.login.failure", "reason": "incompatible_intent", "intent": "login.reciprocate"} + B->>B: Cancel rendezvous + end + A->>B: 3. {"type":"m.login.progress"} + end + end + Note over B: 4. Determine if login_token is available: + B->>+HS: GET /capabilities + HS-->>-B: check supported capabilities for MSC3882 + Note over B: Determine if device authorization grant is available + B->>+OP: GET /.well-known/openid-configuration + OP->>-B: 200 OK {..., "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code", ...]} + alt no protocol available + B->>A: 5. {"type":"m.login.failure", "reason":"unsupported"} + else + Note over B: n.b. that the homeserver is needed to determine the OIDC provider + B->>A: 6. {"type":"m.login.protocols", "protocols":["login_token, "device_authorization_grant], "homeserver": "matrix-client.matrix.org"} + end + + Note over A: 7. Check that suitable protocol available + alt no protocol available + B->>A: {"type":"m.login.failure", "reason":"unsupported", "homeserver": "https://matrix-client.matrix.org"} + else login_token + note over A: Continue as before + else device_authorization_grant + Note over A: Device checks that it can communicate
with the OP. Completing dynamic registration if needed + A->>+HS: GET /.well-known/matrix/client + HS-->>-A: 200 OK {..., "m.authentication": {"issuer": "id.matrix.org"}} + A->>+OP: GET /.well-known/openid-configuration + OP->>-A: 200 OK {..., "device_authorization_endpoint": "https://id.matrix.org/auth/device", ...} + Note over A: Device now knows the OP and what the endpoint is, so then attempts to start the login + A->>+OP: POST /auth/device client_id=xyz&scope=urn:matrix:api:*... + OP->>-A: 200 OK {"user_code": "123456", "verification_url_complete": "https://id.matrix.org/device/abcde", "expires_in_ms": 120000, "device_code": "XYZ", "interval": 1} + + Note over A: At this point A finally gets back to B to tell it what it wants to do: + + B->>A: {"type": "m.login.protocol", "protocol": "device_authorization_grant", "device_authorization_grant": {"verification_url_complete": "https://id.matrix.org/device/abcde"}} + par + Note over A: Device shows the user_code: 1-2-3-4-5-6
and says follow instructions on other device + loop Poll for result at interval seconds. This is standard OIDC stuff: + A->>OP: POST /token client_id=xyz&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=XYZ + alt pending + OP-->>A: 400 Bad Request {"error": "authorization_pending"} + else granted + OP-->>A: 200 OK {"access_token": "...", "token_type": "Bearer", ...} + else denied + OP-->>A: 400 Bad Request {"error": "authorization_declined"} + else expired + OP-->>A: 400 Bad Request {"error": "expired_token"} + end + end + and + Note over B: Device opens verification_url_complete in native browser: + rect rgba(240,240,240,0.5) + B->>OP: GET https://id.matrix.org/device/abcde + OP->>B: consent screen including the user_code + B->>OP: POST /allow or /deny + end + Note over B: browser closed, possibly redirected back to app + end + + Note over A: Device now has an access_token and can start to talk to the homeserver + + Note over A: n.b. the device might already know the device_id due to the way OIDC works. If not then introspect: + A->>HS: GET /whoami + HS->>A: 200 OK {"device_id": "AAABBBCCC"} + + A->>B: { "type": "m.login.success",
"device_key": "zxcxzcxzc",
"device_id": "AAABBBCCC"} + + Note over A,B: 8. Confirmation code should be displayed on both devices e.g 1234-5678-9012 + + Note over B: 9. User asked to approve or deny setup of E2EE on new device + + alt declined + Note over B: or can the OP tell the existing device the outcome? + B->>A: {"type":"m.login.declined"} + else approved + Note over A,B: Do E2EE steps as before: (just showing comms between A and B) + + alt Cross-signing active + B->>A: { "type": "m.login.verified",
"verifying_device_id": "XXXYYYZZZ", "verifying_device_key": "abcdef",
"master_key": } + else No cross-signing + B->>A: { "type": "m.login.verified",
"verifying_device_id": "XXXYYYZZZ", "verifying_device_key": "abcdef" } + end + + end + end +``` + +A UX complication here is that there are two codes that the user needs to see: +the rendezvous checksum; the OIDC device authorization grant code. + +### Usage for launching a new client on a single device + +There are two scenarios in mind: + +1. Showing a "widget" within an existing Matrix client embedded as an iframe/WebView +2. "Popping out"/launching a new Matrix client from inside of an existing Matrix +client + +It could be that the two are go hand-in-hand: a widget could be visible in +embedded mode and a button offered to "open in new window". When clicked a new +browser (or native?) window would be opened which would be signed in and set up +for E2EE as before. + +## Potential issues + +Please also refer to the dependent MSCs. + +## Alternatives + +Please also refer to the dependent MSCs. + +## Security considerations + +Please also refer to the dependent MSCs. + +## Unstable prefix + +Whilst in development the following unstable names should be used: + +- `org.matrix.msc3906.setup.additional_device.v2` for the +`m.setup.additional_device` flow (the `v2` is to distinguish from an earlier +revision of the proposal) +- `org.matrix.msc3906.login_token` for the `login_token` login protocol +- `org.matrix.msc3886.http.v1` for the `http.v1` transport type + +n.b. a previous version of this spec did not include the `flow` field and may be +referred to as `v1` in early implementations. + +## Dependencies + +- [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882) to +obtain a `m.login.token` +- A secure rendezvous channel such as: + - [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) + +[MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903) to do +X25519 over HTTP + - Or in future a guest rendezvous via to_device messaging