Shapes and Colours Considered Insecure
https://franca.lukegrahamlandry.ca
I enjoy writing programs that draw pretty pictures on the screen.
In an ideal world those programs could just be an executable that you
run and then displays the pretty picture.
Unfortunately, if I want to show someone my pretty pictures,
it's a lot of friction to ask them to download an evil native executable.
Much easier instead to ask them to click on a link
that opens a blazingly sandboxed website.
Sadly that too is easier said than done.
The programs I've been writing tend to emphasize (the computation
and the production of the pretty picture) over (having an interface
for doing a task or spending lots time doing nothing as fast as possible
while waiting for another computer, as websites are wont do to).
I also like to tell myself that my programs are not slow which means
I want (threads so I can do lots of things at once), (fragment shaders
so i can do even more things at once), and (to use a language
that can express memory layouts that are efficient for computers to operate on).
The lovely developers of browser technology have answers to my desires
in the form of webworkers, webgpu, and webassembly.
In the name of story telling let's imagine that I've created my
program using all these modern, developer friendly tools.
The last step is to preview it on my computer
to make sure the pictures look right before I release it to the internet at large.
One does not simply "run the program"
For a number of years I've been familiar with the observation that the security of websites
is a decreasing function of the density of pretty pictures they produce.
I have healthy distrust of wenebsnite as a platform
and a corresponding lack of confidence that I'm understanding it correctly,
so if you happen to read this and know that I'm wrong, please tell me.
can i has file? (no)
For trivial website like the one you're reading now,
I can just open the file directly in a browser with a file:/// url.
But it seems that breaks down when you want to reference other (.js, .wasm) files in the directory.
For example, adding a script tag with type="module" yields the following:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:///[...]. (Reason: CORS request not http).
Module source URI is not allowed in this document: “file:///[...]”. index.html:86:45
As a foreshadowing of things to come, typing SharedArrayBuffer in the console will happily report that it's undefined.
U+201C story time
Such joyous memories triggered by pasting the curly quote from that error message.
In university I had an assignment that involved writing a kernel module
that would register a character device that would return back whatever the last byte written to it was.
The command they typed in a pdf to test it was something like
echo -n “a” > /dev/foo
which when pasted into bash may disappoint the uninitiated who later read 0x9d instead of 0x61.
can i has server? (no)
python -m http.server 8000
Now I can go to http://localhost:8000 to see an exciting change to the errors in the console.
The WebAssembly.Memory object cannot be serialized. The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers can be used to enable this.
A quick consultation with https://developer.mozilla.org yields conformation.
WebAssembly.Memory: "The WebAssembly.Memory object is a resizable ArrayBuffer or SharedArrayBuffer"
SharedArrayBuffer: "To use shared memory your document must be in a secure context and cross-origin isolated."
WebGPU API: "Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers."
The web people are having a MELTDOWN over spooky ghosts and SPECTREs (the joke is funny because...).
Which is fair, hard to argue with that, but means more work for me.
why meltdown over counting up?
The big threat they're protecting me from is that if you can spawn a thread
that spams atomic increments on a shared memory address,
another thread can use that as a high precision timer.
And like ok so what but with a timer you can observe whether a memory access is a cache hit
and with speculative execution your cpu allows you to put specific memory locations in cache based
on a value stored in memory you shouldn't be able to read.
Then all of a sudden you can turn arbitrary code execution into arbitrary memory reads.
Which sounds a bit dumb when you say it like that but websites were supposed to be blazingly sandboxed
so it's a big deal to be able to leak something through that.
You can ask dear wikipedia
for a less flippant explanation, it's very cool if you're in to that sort of thing.
can i has security? (yes)
why no --tls-(cert, key)?
I happen to have python 3.13.7, but in 3.14 they added --tls-cert and --tls-key parameters to the command above
which would let me get father without writing a program, but would still need the openssl
incantation to generate the keys and wouldn't fix the headers problem so i'd still need the program anyway
to make it actually work.
Luckily it's easy to use http.server as a library instead of a script and wrap it in an SSLContext
(I stole this code the old fashioned way).
Then it's only a few extra lines to poke in the magic Cross-Origin-foo headers.
# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout target/key.pem -out target/cert.pem
HOST = "localhost"
PORT = 8000
import http.server
import ssl
import os
class Handler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
super().end_headers()
os.chdir("target/web")
with http.server.HTTPServer((HOST, PORT), Handler) as s:
print("https://" + HOST + ":" + str(PORT))
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(keyfile="../key.pem", certfile="../cert.pem")
s.socket = ctx.wrap_socket(s.socket, server_side=True)
s.serve_forever()
Using a self signed certificate like that means there's a scary warning the first time I open it in a browser but I'm well past the point of caring about that.
This attempt does work, but it makes me sad...
can i has smaller dependencies? (yes)
I have perhaps an unhealthy obsession with my programs being self contained.
Let's just gloss over the fact that this whole endeavour means depending on a bazillion lines of chrome.
I don't like the aesthetics of hoping the openssl executable is globally installed somewhere convenient.
If I wanted to commit to python, I could presumably use it to generate the certificate instead but
running (pip install cryptography) is kinda the opposite direction of where I want to go.
As a more concrete motivation I don't want to add openssl or python to my tests/flake.nix
but I do want to be able to run (nix develop ./tests) and see my website somehow.
The
first step is replacing openssl with mbedtls,
which from past experiments I know can be compiled by
my c compiler.
The command before used the openssl binary for two things: generating an rsa key and generating a tls certificate.
One of the mbedtls example programs
cert_write.c
shows how to do the latter and the former is very similar to an example from their
getting started docs with different magic constants and a bit of extra fiddling around to use mbedtls_pk_write_key_pem.
So with that I can produce key.pem and cert.pem files that work with the python program from before.
As a matter of principle I'd also like to replace the python program with one written in
my language.
Mbedtls also has an example of a minimal server that just returns a hard coded result for every request:
ssl_server.c.
It turns out that once someone else handles the cryptography/handshake/whatever part for me, the http part is trivial.
The gist is use the socket,bind,listen syscalls to get a file descriptor for a new socket,
then the accept syscall gives me a new file descriptor when someone makes a request.
After calling mbedtls_ssl_handshake, accessing that last fd goes through
mbedtls_ssl_read/mbedtls_ssl_write instead of making the read/write syscalls directly.
The requests coming in look something like:
GET /[path] HTTP/1.1\r\n
Host: [localhost:8000]\r\n
[more header lines]\r\n
\r\n
Then response I send back is something like:
HTTP/1.0 200 OK\r\n
Content-Type: [mime]\r\n
[more header lines]\r\n
\r\n
[data]
with the [data] from reading the [path] on disk.
The Content-Type header is important for application/wasm and application/havascript or the browser won't run the code.
The headers (Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy) are also needed as discussed before.
I also send Content-Length because I use it for a progress indicator in the ui the first time the wasm is loading.
In the end I have a new command that generates the key/cert if missing and then runs the https server like before:
franca examples/web/serve.fr port 8000 -folder target/web
I acknowledge that mine is probably slower than the python one.
Unlike the programs that bring me joy, this one spends all of its time doing nothing as fast as possible.
Note again that this is all to use the web browser on my own computer to get the program that I wrote that lives on my own computer to run so I can test it.
I care zero for security, it's just my computer talking to itself.
If there's some terrible vulnerability in the version of mbedtls I pinned, that's fine, it's just my computer talking to itself.
If I did a bug because I wrote it in a evil unsafe language, that's fine, it's just my computer talking to itself.
Your millage may vary. This is not medical advice.
For the time being I'll leave it here and keep using mbedtls rather than trying to roll my own of that too...
rome wasn't built in a day as they say.
can i has deploy? (yes)
Now that I can test my program, the easiest way to give it to the internet
is to just commit some yaml junk and have github pages do it.
Unfortunately they don't set the http headers to make it cross-origin isolated so it doesdendt' work :(.
Putting it behind cloudflare lets me have an "HTTP Response Header Transform Rule" that adds them.
This is good because it means that my website can die if either microslop OR clownflare die instead of just depending on one.
But in all fairness I'm too lazy to do it myself so I'm not exactly at peak moral high ground for complaining.
I also tried srht.site which is much less painful but it also doesn't set the magic header
and cares not for cloudflare, which I agree with in principle but means I can't cheat and set them myself.
The website I've been trying to make happen this whole time
is a playground for my language that jit compiles my various toy programs to wasm.
There's a link to the live version at the top of this page if you want to try it.
It's a bit of a gamble which browsers it works in;
may the force be with you and with your spirit.
prev: Prospero Challenge Entry
change log
[Apr 27, 2026] wrote the words