Loop/Architecture/Fingerprint Validation
WebRTC's communications security model works by way of a three-pronged model:
- All media is encrypted with SRTP-DTLS, which uses a DH exchange to establish media security. The negotiated DH keys are not exposed to content.
- To prevent active on-path attacks, DTLS fingerprint validation is performed by using a third-party signature, rooted in the web PKI, to authenticate the fingerprint. See the WebRTC 1.0 Identity mechanism for details.
- To prevent in-content attacks, any streams for which fingerprint validation has been performed are "isolated" such that the content cannot be extracted by the webpage.
The first mechanism prevents passive interception; the second, active on-path interception; and the third, attacks by the content itself.
Currently, Hello makes use of only the first of these mechanisms, and relies on the security of the underlying network infrastructure (specifically, the OpenTok servers) to prevent on-path attacks. Because the nature of Hello does not require the use of any identity, making use of the second and third levels of protection can't be employed as specified.
It is worth noting that additional trust gained by adding the Identity mechanism is only as good as the trust provided by the party that is vouching for a user's identity. For example, if the corresponding service cannot be trusted to provide such certificates only to authenticated users (or cannot be trusted to properly validate certificates when asked to), then its role is largely defeated.
This basically sets up a system in which the Identity provider is serving as a third-party check against attacks on the infrastructure used to exchange SDP (and therefore DTLS fingerprints).
The design below provides additional protection by using Mozilla's servers to provide a third-party check against attacks on the OpenTok infrastructure, without requiring the use of verifiable user identities. Note that this isn't as strong as the protection provided by the WebRTC Identity mechanism; however, it is a significant improvement over the existing situation, as it would require attacks on both the OpenTok infrastructure and Mozilla's infrastructure to launch an active interception of content.
Contents
API Changes
This design proposes minimal changes to the Loop server protocol to allow clients to upload valid fingerprints for the room they are joining. At a high level, we expand the data maintained for a user in a room to include an array of fingerprints. Each fingerprint corresponds to an active PeerConnection associated with the room.
The following examples replace "displayName" with "-", as that is the behavior currently exhibited by the Hello client.
Joining a Room
For this scheme to work in a backwards-compatible fashion, the participants in a conversation need to know whether to expect their peers to be publishing fingerprints. For this, we add a simple capabilities announcement mechanism when users join a room. This is an additional parameter on the messages described at Loop/Architecture/Rooms#Joining a Room:
POST /rooms/QzBbvGmIZWU HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate Content-Type: application/json; charset=utf-8 Authorization: <stripped> Host: localhost:5000 { "action": "join", "displayName": "-", "clientMaxSize": 2, "features": ["fingerprint"] }
- features: An array of optional client behaviors that depend on the version of the client. Currently, only one value is defined:
- fingerprint: Indicates support of the PeerConnection fingerprint validation mechanism. If this mechanism is present when the user joins a room, then the server must include a "fingerprints" array (which will initially be empty) as part of the user identification information. If absent, the server must not include such an array.
User Identification in a Room
This design expands the information defined by Loop/Architecture/Rooms#User Identification in a Room to include an array of PeerConnection fingerprints. Note that this array MUST be present (even when empty) if the client indicated a "fingerprint" feature when joining the room. It MUST NOT be present if the client didn't indicate support for "fingerprint".
{ "displayName": "-", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb", "fingerprints": [] }
- fingerprints: A list of "fingerprint" values associated with all the PeerConnections the client currently has in use. Only included if client included "fingerprint" in features array in "join"
Uploading PeerConnection Fingerprints
This design adds a new action to the POST /rooms/{token} endpoint; this action is called "add-fingerprint". Clients supporting fingerprint validation will send this information whenever a new PeerConnection begins media negotiation.
POST /rooms/QzBbvGmIZWU HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate Content-Type: application/json; charset=utf-8 Authorization: <stripped> Host: localhost:5000 { "action": "add-fingerprint", "fingerprint": "sha-256 15:E2:AF:50:91:87:FD:54:4C:82:F5:65:46:7A:84:D8:6C:53:00:99:C6:97:4E:64:2A:32:AA:A5:3C:91:E9:51" }
- action - For sending a new PeerConnection fingerprint, this will be "add-fingerprint".
- fingerprint - The new PeerConnection fingerprint. The server will store this fingerprint as part of the user's identity in the room, and return it as an element in its "fingerprints" array. Fingerprint values should be de-duplicated by the server, and uploading the same fingerprint multiple times shall not be an error.
Retrieving Room Information
This section doesn't define any new behavior; it simply shows an example that is the result of combining the previous three sections. It is an amended version of the response shown at Loop/Architecture/Rooms#GET /rooms/{token}
HTTP/1.1 200 OK Connection: keep-alive Content-Length: 30 Content-Type: application/json; charset=utf-8 Date: Wed, 16 Jul 2014 13:23:04 GMT ETag: W/"1e-2896316483" Timestamp: 1405516984 { "roomToken": "3jKS_Els9IU", "roomName": "UX Discussion", "roomUrl": "http://localhost:3000/rooms/3jKS_Els9IU", "roomOwner": "-", "maxSize": 2, "clientMaxSize": 2, "creationTime": 1405517546, "ctime": 1405517824, "expiresAt": 1405534180, "participants": [ { "displayName": "-", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb", "fingerprints": [ "sha-256 15:E2:AF:50:91:87:FD:54:4C:82:F5:65:46:7A:84:D8:6C:53:00:99:C6:97:4E:64:2A:32:AA:A5:3C:91:E9:51", "sha-256 92:4B:E6:3C:DE:41:D6:F6:4A:F8:37:EC:44:3E:71:76:F3:4D:AC:7D:9C:21:6F:A9:37:5B:33:E5:9D:E2:7F:C0" ] }, { "displayName": "-", "roomConnectionId": "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7", "fingerprints": [ "sha-256 87:C1:3C:5C:CB:D0:B6:86:3C:6E:A9:BF:CF:12:CD:F9:3F:37:95:B0:8C:3E:03:A1:6B:85:D7:B4:A4:22:1F:30", "sha-256 23:5E:B5:28:CF:2D:9F:D3:09:EE:E2:2F:D8:EF:DD:05:FA:FF:41:AB:1F:81:1F:73:21:E7:24:40:45:F1:8E:D4" ] } ] }
Client Behavior
Much of the required client behavior is implied by the network API; however, to be explicit, this section describes the expected client behavior.
Prior to loading the OpenTok SDK, the client will override window.RTCPeerConnection (or the appropriate prefixed variant, if present). The overridden function will call the original constructor, override the setLocalDescription and setRemoteDescription methods on the newly created object, and return the modified PeerConnection. The overrides of setLocalDescription and setRemoteDescription will perform exfiltration and comparison of fingerprints, respectively. See #Proof-of-Concept Monkeypatch Shim for an example of how this might look.
In more detail: when the setLocalDescription shim is called, it extracts the fingerprint attribute from the SDP that was passed to that function, and enqueues it to be sent to the Loop server (e.g., using setTimeout(...,0)). The fingerprint is sent using a "POST /rooms/{token}" request, with action="add-fingerprint", as described in [#Uploading PeerConnection Fingerprints]
When the setRemoteDescription shim is called, it enqueues a function with a relatively short timeout (I propose 5 seconds) that performs the following steps:
- Check the list of fingerprints published by other person in the room.
- If the user's information doesn't contain a fingerprint array, then it doesn't support fingerprint validation. Initially, this will result in simply skipping the rest of this procedure. In the future, we will want to treat this as a client that is to out-of-date to interoperate with current clients. The timeframe for doing so is for future study.
- If no matching fingerprint is found, perform a "GET /rooms/{token}" to refresh the room information information
- When the response to that GET is received, tries again to find the fingerprint in the user's information
- If no match is still found, the client will:
- Log an error to the Loop server indicating a fingerprint validation error
- Alert the user that the session is being terminated because its security cannot be verified, and
- Terminate the session
Proof-of-Concept Monkeypatch Shim
This is a very simple example that shows how to override the setLocalDescription and setRemoteDescription methods as required to implement this design. It has been verified not to interfere with the OpenTok SDK's proper functioning. This version is Firefox-specific; however, it can presumably be modified to work in Chrome by simply checking for and using the "webkit" prefix as necessary.
window._originalRTCPeerConnection = window.mozRTCPeerConnection; window.mozRTCPeerConnection = function() { var setDescriptionShim = function(sdp, success, failure, pc, localRemote) { var fingerprint = /a=fingerprint:([^\r\n]*)/.exec(sdp.sdp)[1]; console.log(localRemote + " fingerprint = " + fingerprint); // Use any available event-emitter to decouple this code from the application. eventEmitter.emit("set" + localRemote + "Description", sdp); pc["_originalSet" + localRemote + "Description"](sdp, success, failure); } var pc = new window._originalRTCPeerConnection(); pc._originalSetLocalDescription = pc.setLocalDescription; pc._originalSetRemoteDescription = pc.setRemoteDescription; pc.setLocalDescription = function(sdp, success, failure) { setDescriptionShim(sdp, success, failure, pc, "Local"); } pc.setRemoteDescription = function(sdp, success, failure) { setDescriptionShim(sdp, success, failure, pc, "Remote"); } return pc; }