diff --git a/proposals/4108-oidc-qr-login.md b/proposals/4108-oidc-qr-login.md
index 76821053..461a0e1e 100644
--- a/proposals/4108-oidc-qr-login.md
+++ b/proposals/4108-oidc-qr-login.md
@@ -788,8 +788,16 @@ At this point the new device knows that, subject to the user consenting, it shou
3. **New device informs existing device that it wants to use the `device_authorization_grant`**
-The new device send the `verification_uri` and, if present, the `verification_uri_complete` over to the existing device and
-indicates that want to use protocol `device_authorization_grant` along with the `device_id` that will be used:
+At this point, the new device should ensure it has generated its Olm account, so that it has its Curve25519 and Ed25519
+device identity keys.
+
+It then sends a `m.login.protocol` message to the existing device, containing:
+
+- An indicator that it wants to use protocol `device_authorization_grant`
+- The `verification_uri`
+- The `verification_uri_complete`, if present
+- The device ID it will be using, which MUST equal the unpadded base64-encoded form of the Curve25519 identity key of
+ the new device
*New device => Existing device via secure channel*
@@ -801,7 +809,7 @@ indicates that want to use protocol `device_authorization_grant` along with the
"verification_uri": "https://auth-oidc.lab.element.dev/link",
"verification_uri_complete": "https://auth-oidc.lab.element.dev/link?code=123456"
},
- "device_id": "ABCDEFGH"
+ "device_id": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"
}
```
@@ -848,7 +856,7 @@ sequenceDiagram
N->>+OP: POST /auth/device client_id=xyz&scope=openid+urn:matrix:api:*+urn:matrix:device:ABCDEFGH...
OP->>-N: 200 OK {"user_code": "123456",
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"expires_in_ms": 120000, "device_code": "XYZ", "interval": 1}
note over N: 3) New device informs existing device of choice of protocol:
- N->>Z: SecureSend({"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...}, "device_id": "ABCDEFGH"})
+ N->>Z: SecureSend({"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...,
"device_id": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"})
deactivate N
end
@@ -860,7 +868,7 @@ sequenceDiagram
end
rect rgba(0,255,0, 0.1)
- Z->>E: SecureReceive() => {"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...}, "device_id": "ABCDEFGH"}
+ Z->>E: SecureReceive() => {"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...},
"device_id": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"}
end
rect rgba(255,0,0, 0.1)
@@ -918,7 +926,7 @@ sequenceDiagram
N->>+OP: POST /auth/device client_id=xyz&scope=openid+urn:matrix:api:*+urn:matrix:device:ABCDEFGH...
OP->>-N: 200 OK {"user_code": "123456",
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"expires_in_ms": 120000, "device_code": "XYZ", "interval": 1}
note over N: 3) New device informs existing device of choice of protocol:
- N->>Z: SecureSend({"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...}, "device_id": "ABCDEFGH"})
+ N->>Z: SecureSend({"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...},
"device_id": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"})
deactivate N
end
@@ -929,7 +937,7 @@ sequenceDiagram
#end
rect rgba(0,255,0, 0.1)
- Z->>E: SecureReceive() => {"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...}, "device_id": "ABCDEFGH"}
+ Z->>E: SecureReceive() => {"type": "m.login.protocol", "protocol": "device_authorization_grant",
"device_authorization_grant":{
"verification_uri_complete": "https://id.matrix.org/device/abcde",
"verification_uri": ...},
"device_id": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"}
end
# alt if New device scanned QR code
@@ -953,10 +961,10 @@ Then we continue with the actual login:
On receipt of the `m.login.protocol_accepted` message:
- In accordance with [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628#section-3.3.1) the new device must display
-the user_code in order that the user can confirm it on the OIDC Provider if required.
+the `user_code` in order that the user can confirm it on the OIDC Provider if required.
- The new device then starts to poll the OIDC Provider by making
[Device Access Token Requests](https://datatracker.ietf.org/doc/html/rfc8628#section-3.4) using the interval and bounded
-by expires_in.
+by `expires_in`.
*New device => OIDC Provider via HTTP*
@@ -1028,7 +1036,7 @@ sequenceDiagram
OP-->>N: 400 Bad Request {"error": "authorization_pending"}
else granted
OP-->>N: 200 OK {"access_token": "...", "token_type": "Bearer", ...}
- N->>E: SecureSend({ "type": "m.login.success" })
+ N->>E: SecureSend({ "type": "m.login.success", "proof": proof_of_identity_key_ownership })
Note over N: Device now has an access_token and can start to talk to the homeserver
else denied
OP-->>N: 400 Bad Request {"error": "authorization_declined"}
@@ -1051,6 +1059,9 @@ sequenceDiagram
end
```
+The reader will note that the `m.login.success` contains a proof that the new device owns the identity key it had
+previously committed to, in the `m.login.protocol` step. This is explained in the next section.
+
#### Secret sharing and device verification
Once the new device has logged in and obtained an access token it will want to obtain the secrets necessary to set up
@@ -1067,7 +1078,7 @@ If checked successfully then the existing device sends the following secrets to
This is achieved as following:
-1. **Existing device confirms that the new device has indeed logged in successfully**
+1. **Existing device confirms that a device with the previously committed-to device ID (device identity key) has indeed logged in successfully**
On receipt of an `m.login.success` message the existing device queries the homeserver to check that the is a device online
with the corresponding device_id (from the `m.login.protocol` message).
@@ -1079,9 +1090,40 @@ If the device isn't immediately visible it can repeat the `GET` request for up t
If no device is found then the process should be stopped.
-2. **Existing device shares secrets with new device**
+2. **Existing device confirms that the new device owns the private part of the committed-to device identity key**
+
+The new device then proves it controls the public key to which it previously committed. It does this by doing an ECDH
+between the committed-to identity key and the other device's secure channel ephemeral key to derive a shared secret,
+which is used to construct a proof of ownership. Due to the properties of ECDH, the other device knows that the new
+device can only do this if it possesses the private part of the committed-to identity key.
+
+The new device does:
+
+```
+SH := ECDH(Is, Ep)
+EncKey_Proof := HKDF_SHA256(SH, "MATRIX_QR_CODE_LOGIN_ENCKEY_PROOF|" || Ip || "|" || Ep, salt=0, size=32)
+NonceBytes := ToLowEndianBytes(0)[..12]
+Proof := ChaCha20Poly1305_Encrypt(EncKey_Proof, NonceBytes, "MATRIX_QR_CODE_PROOF_OF_POSSESSION")
+```
-The existing device sends a `m.login.secrets` message via the secure channel:
+And sends the **Proof** to the existing device.
+
+The existing device does the following to verify the proof:
+
+```
+SH := ECDH(Es, Ip)
+EncKey_Proof := HKDF_SHA256(SH, "MATRIX_QR_CODE_LOGIN_ENCKEY_PROOF|" || Ip || "|" || Ep, salt=0, size=32)
+NonceBytes := ToLowEndianBytes(0)[..12]
+Plaintext := ChaCha20Poly1305_Decrypt(EncKey_Proof, NonceBytes, Proof)
+
+unless Plaintext == "MATRIX_QR_CODE_PROOF_OF_POSSESSION":
+ FAIL
+```
+
+3. **Existing device shares secrets with new device**
+
+If both previous steps succeeded, the existing device proceeds to send a `m.login.secrets` message via the secure
+channel:
```json
{
@@ -1122,14 +1164,14 @@ Content-Type: application/json
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
- "device_id": "SGKMSRAGBF",
+ "device_id": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
"keys": {
- "curve25519:SGKMSRAGBF": "I11VOe5quKuH/YjdOqn5VcW06fvPIJQ9JX8ryj6ario",
- "ed25519:SGKMSRAGBF": "b8gROFh+UIHLD/obY0+IlxoWiGtYVhKdqixvw4QHcN8"
+ "curve25519:3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
+ "ed25519:3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI": "b8gROFh+UIHLD/obY0+IlxoWiGtYVhKdqixvw4QHcN8"
},
"signatures": {
"@testing_35:morpheus.localhost": {
- "ed25519:SGKMSRAGBF": "ziHEUIsHnrYBH4CqYpN1JC/ex3t4VG3zvo16D8ORqN6yAErpsKsnd/5LDdZERIOB1MGffKGfCL6ny5V7rT9FCQ",
+ "ed25519:3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI": "ziHEUIsHnrYBH4CqYpN1JC/ex3t4VG3zvo16D8ORqN6yAErpsKsnd/5LDdZERIOB1MGffKGfCL6ny5V7rT9FCQ",
"ed25519:bkYgAVUNqvuyy8b1w09utJNJxBvK3hZB65xxoLPVzFol": "p257k0tfPF98OIDuXnFSJS2DmVlxO4sgTHdF41DTdZBCpTZfPwok6iASo3xMRKdyy3WMEgkQ6lzhEyRKKZBGBQ"
}
},
@@ -1276,7 +1318,7 @@ Fields:
|`type`|required `string`|`m.login.failure`|
|`reason`|required `string`| One of:
Value | Description |
authorization_expired | The Device Authorization Grant expired |
device_already_exists | The device ID specified by the new client already exists in the Homeserver provided device list |
device_not_found | The new device is not present in the device list as returned by the Homeserver |
unexpected_message_received | Sent by either device to indicate that they received a message of a type that they weren't expecting |
unsupported_protocol | Sent by a device where no suitable protocol is available or the requested protocol requested is not supported |
user_cancelled | Sent by either new or existing device to indicate that the user has cancelled the login |