TLS Over Websocket
But Why? Be-CORS, that's why.
I was writing an unofficial light-weight browser experience for members of an online comedy channel. I'd reversed their API and navigated the simple enough OAuth implementation they had, but then ran up against the issue of CORS.
tv.bonner.isisn't allowed to communicate with
Now, the easy fix is by simply writing up a forwarding-proxy at
red.bonner.is that looks something like this:
The rough gist is that someone can make a request to my site, my server will make a request on their behalf and respond with the result. It avoids CORS because I can tell
bonner.is to accept any requests. The problem, is that my site can see the user's password as it's sent over, and that's a BAD THING™.
It's simple, we implement
auth.comedy-channel.com offers the ability to open a TLS channel on their port 443. This is what the
https part of a URL represents when you visit a site.
Transport-Layer-Security (TLS) is a protocol that allows two people to communicate, even when passing all the messages via a third-party. You can read up on my rough summary of the underpinning math here. The important part is that it's a protocol that's communicated over TCP. What this means is that a series of packets are exchanged between your computer and
auth.comedy-channel.com's port 443 over TCP which then establish a TLS channel, over which an HTTP request is made.
In the browser we have a few ways to communicate with other computers. One is the simple
fetch which allows us to make HTTP calls. But there isn't a way to renegotiate a TLS exchange from HTTP (wait, what?! No...) so this isn't an option.
Another is WebRTC which uses SCTP which is a amalgamation of both UDP and TCP. This is maybe an option given it'd be possible to create a bridge between the ports to allow the TLS stream to be setup.
But the next and most interesting choice is WebSockets, which allows us to upgrade to a bi-directional data-stream with an accepting HTTP endpoint. An amazing project called WebSocketify exists which has the following description:
Websockify is a WebSocket to TCP proxy/bridge.
Bingo. What they're saying is that they'll accept a WebSocket connection, and convert the data-stream into a TCP stream to a target port.
All we have to do is setup the Websockify server on our server, this is done in my setup via a
docker-compose setup found here:
With the docker file looking like this:
# Initialize FROM python:3 # Install WORKDIR /app RUN apt-get update && apt-get install --yes net-tools RUN pip install --no-cache-dir numpy RUN git clone https://github.com/novnc/websockify /app/websockify # Run-time # ENTRYPOINT [ "ls", "/app/websockify" ] RUN chmod +x /app/websockify/run ENTRYPOINT bash /app/websockify/run -v 6060 "$TARGET"
The net result is that we end up with a sub-domain that accepts WebSocket connections, which will proxy the incoming raw byte-stream over to our target site's port 443 as a TCP stream.
Given we can now send a byte-stream to
auth.comedy-channel.com:443 all we need to perform an HTTPS POST request. This is made up of two parts:
- A POST request, written as a simple ASCII payload.
- A TLS handshake between the client and the server.
The first is trivial and can be done in Notepad, the second requires tens of thousands of lines of code.
After a bit of hunting, we find the
digitalbazaar, who've written a library called
Which allows us to throw together the following library:
FetchOverWebSockets provides us with the ability to run the following code:
So, to summarise. We establish a secure WebSocket connection to
bonner.is which proxies a byte-stream to
comedy-channel.com:443, then we perform a TLS hand-shake between client and the end-server, and then finally POST a login request.
All completely secure from the clients machine.
All of this should have the large caveat that the client trusts the code sent to it, so unless this was authenticated via a third-party the whole point is moot, but it was very fun to put together. I'm fairy sure the server would be unable to MITM this connection given it's end-to-end encrypted in the sub-stream.