From 0894c770ee41e34846003ecc65836aa5833244f5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Jul 2025 11:12:54 -0700 Subject: [PATCH 1/2] Add a blog for TLS. --- src/pages/blog/tls-and-quic.mdx | 295 ++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/pages/blog/tls-and-quic.mdx diff --git a/src/pages/blog/tls-and-quic.mdx b/src/pages/blog/tls-and-quic.mdx new file mode 100644 index 0000000..f4d53d0 --- /dev/null +++ b/src/pages/blog/tls-and-quic.mdx @@ -0,0 +1,295 @@ +--- +layout: "@/layouts/global.astro" +title: TLS and QUIC: A Masochist's Guide +author: kixelated +description: Setting up TLS is a pain, but it's a requirement for HTTPS. QUIC and WebTransport introduce even more pain so it's time to be a masochist. +cover: "/blog/replacing-webrtc/artifact.png" +date: 2025-07-28 +--- + + +# TLS and QUIC: A Masochist's Guide +I hope you're having a great day. +The sun is about to shine brighter. +I was inspired by Helios himself to write about the riveting topic of TLS and QUIC. + +In my opinion, the most difficult part about QUIC is setting up a different protocol: QUIC *requires* TLS. +If you screw it up, you'll get a scary ERROR screen and users won't be able to connect. +This guide also applies to HTTPS and WebTransport... with some important distinctions. + + +tl;dr +- TLS authenticates who is allowed to serve `example.com` via certificates. +- Root CAs issue certificates to those who can prove it via DNS. +- Cloud offerings don't support QUIC yet, so that rules out the easy options. +- TLS is hell for local development, _especially_ for WebTransport. + + +## About Me +I'm not a security engineer but I have dabbled in the low-level protocols. +For some unbeknownst reason, I've implemented both DTLS 1.2 (for WebRTC) and TLS 1.3 (for QUIC)... in Go. +Both were undoubtedly insecure but somehow passed the security audit and served production traffic for weeks... before getting the `rm -rf` treatment. + +When in doubt, always refer to the nerds who take security seriously and use the correct terminology. +I understand a lot of the security primitives but I don't exactly have the LinkedIn Professional Certificates to back it up. + + +## Why TLS? +*Some boring background, feel free to skip ahead.* + +TLS is a client-server protocol that is used to verify the identity of the server (and optionally the client: mTLS) before establishing an encrypted connection. +When a client connects to `example.com`, the server transmits proof that it "owns" `example.com`. +This proof is in the form of a TLS certificate (technically [X.509](https://en.wikipedia.org/wiki/X.509)) which can be used to "sign" stuff by solving a math problem that is super super difficult without access to a "private key". +TLS certificates can be used to sign other TLS certificates creating a "chain of trust". + +Without TLS, an attacker could intercept your traffic and pretend to be `example.com` to harvest your credentials, called a "man-in-the-middle" (MITM) attack. +Imagine if your router, ISP, or fellow coffee shop customer could pretend to be your bank. +Bad times ahead. + +QUIC requires [TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446). +There is no way to disable encryption but depending on the client, you can modify certificate validation. +The IETF grey-beards decreed that the protocol can never be insecure, lest a lowly application developer shoot themselves in the foot. + +- HTTP/3, WebTransport, and of course, Media over QUIC all use QUIC under the hood so all of this applies. +- HTTP/2 *technically* does not require TLS but browsers require it as a forcing function. +- HTTP/1 is the lone exception, allowing you to choose if you want to connect to `http://example.com` (insecure) or `https://example.com` (TLS). + +Not only is TLS required when using newer protocols, it's often required when using newer browser APIs. +[Secure Context APIs](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts) require using TLS or a localhost connection. +Want to do literally anything? Then you need to use HTTPS. + +So yeah, browser vendors don't want you to make a security oopsie whoopsie and thus, effectively mandate TLS +But there's very little reason to use an insecure connection anyway so suck it up and let some cloud provider handle TLS for you. + +...unless you're one of the early birds using QUIC... + +*ominous foreshadowing* + + +# Certificate Validation +At a high level, a TLS connection is established via: + +1. The client connects to `example.com` and sends a `ClientHello`. + - The client (usually) sends the domain via a [SNI extension](https://en.wikipedia.org/wiki/Server_Name_Indication). +2. The server transmits a `ServerHello` along with a TLS certificate signed for `example.com`. + - The server can use SNI to choose the certificate to serve. +3. The client verifies that the TLS certificate is for `example.com`. +4. The client verifies that the TLS certificate was signed/created by a "root CA". + + +What is a root CA? +Your browser and operating system ship with a list of trusted entities that are authorized to issue certificates. +It's like having a list of approved locksmith companies that can make keys for any house. +The list changes over time as these companies are audited or catastrophically compromised. +If you've ever had to `apt install ca-certificates` when setting up a Docker image, this is why. + +One of the interesting things about TLS is that the client can choose how to verify the provided TLS certificate. +There's no requirement that you use these provided root CAs, or a root CA at all. +You can make the protocol as secure/insecure as you want if you have enough control over the client. + +So in order to run a QUIC server on the internet, you need to get access to a certificate signed for a specific domain name (or IP address). +If your server listens on `example.com`, then we need to prove to some root CA that you own `example.com` + + +## Cloud Offerings +The easiest option unfortunately doesn't work for QUIC. +Poop. + +Virtually every cloud provider offers HTTPS support. +You point your domain name at their load balancer and they procure a TLS certificate. +But I glossed over something, there's actually two parts to a "TLS certificate": the private key and the certificate. + +None of these services actually give you, the customer, the private key. +Instead, they run a HTTPS or TLS load balancer that terminates TLS and proxies unencrypted traffic to your backend. +It's done for simplicity and security, they don't want you having access to the keys. + +This won't work for QUIC until cloud providers start offering QUIC load balancers (one day). +Welcome to UDP protocols; they're barely supported on cloud platforms because TCP and HTTP is so widespread. +At least there's hope for QUIC support in the future *because* it powers HTTP/3. + + +## LetsEncrypt +The recommended path is to use the glorious [LetsEncrypt](https://letsencrypt.org/) to get a certificate. +It's free, it's painless, and it's highly recommended by yours truly. +There are paid offerings too, but there's really not much point using them since LetsEncrypt became a thing. + +How this works is that you need to somehow prove to LetsEncrypt that you own a specific domain and then they'll give you a certificate valid for 90 days. +"owning" a domain in this context means you control the ability to add DNS records. +This could be in the form of an A record that points to an IP address you own or a TXT record with some token. + +There are a few different [challenge types](https://letsencrypt.org/docs/challenge-types/): +- *HTTP-01*: Host a HTTP endpoint (not using TLS) that returns a specified token on the specified path. You own the domain if it points to your HTTP server. +- *DNS-01*: Create a DNS record with a specified token. You own the domain if you can create this TXT record; no server required. +- *TLS-ALPN-01*: Host a TLS endpoint that returns a specified token during the TLS handshake. Same idea as HTTP-01, but often more convenient because no HTTP server is required. + +This cert generation can be automated via [certbot](https://certbot.eff.org/) or any [ACME library](https://letsencrypt.org/docs/client-options/#libraries). +It's a little bit annoying because of the 90 day expiration; any long-lived servers will need to periodically restart or reload certificates + +## ACME +It would be remiss of me to not mention that LetsEncrypt uses [ACME v2](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment). +You don't need to use `certbot` and in fact you could integrate ACME directly into your workflow. + +In fact, I'm using a **not recommended** [terraform module](https://github.com/vancluever/terraform-provider-acme) to generate certificates for `relay.quic.video`. +It's not recommended because if I forget to run terraform every so often, then oops, my certificate will expire and users can no longer connect to my server. + +I'm also considering using a [ACME library](https://crates.io/crates/instant-acme) within `moq-relay` itself to provision a TLS certificate on startup and every 90 days. +This makes it a lot easier to use cloud providers as you don't need to fumble with background services (in Docker, ew) and reloading the server whenever a new certificate is generated. +The downside is being unable to generate wildcard certificates as those require DNS challenges. + + +# Private Networks +Anyway, we've talked a lot about the internet, but what about the intranet? + +The problem with a service like LetsEncrypt is that it requires our server to be public. +What if we're running our own private network or just developing an application locally? +If LetsEncrypt can't connect to our private network, then it can't give us a TLS certificate. + +Additionally, LetsEncrypt requires a domain name and potentially a public IP. +These aren't expensive but it's not something you can reasonably ask developers to purchase just to try running your code. + +Remember when I said the client was responsible for verifying a certificate however it sees fit? +If we control the QUIC client, then we can change that behavior. + +**NOTE**: None of this currently applies to WebTransport as we don't have enough control of the browser client. +See the next section! + + +## Disable Verification +The most obvious thing the client can do is skip verification altogether. +There's usually a **DANGER** warning associated and **DANGER** indeed. + +If you skip certificate validation, your connection will still be encrypted but now it's vulnerable to a MITM attack. +The server still has to present a certificate, but the client will blindly accept *any* certificate. +Even if it was generated nanoseconds ago, was riddled with typos, and claimed to be `pornhub.com`: doesn't matter. + +`moq-relay` supports the [--tls-disable-verify](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/client.rs#L22) by [jumping through a few hoops with rustls](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/client.rs#L202). +There's similar flags in most CLI tools, like curl's `--insecure` flag. + + +## Custom Root CAs +Instead of using a "trusted" root CA that ships with the browser or operating system, we can use our own. +We can trivially generate our *own* root CA which can then be used to sign our *own* certificates. +No more depending on FAANG, we are our own auditor now. + +This is arguably safer than using public roots because our own client can be configured to ONLY accept our certificates. +It doesn't matter if one of the many public CAs gets hacked as long as our root is kept under lock and key. +But note that you're now responsible for a lot more infrastructure, like the ability to revoke compromised certificates. + +The catch is that we need to configure clients to trust our root CA. +This normally requires admin/root access, and can be done at an operating system or browser level. +But you only have to do it once per root CA and then you're golden. + +This is the secret behind a tool like [mkcert](https://github.com/FiloSottile/mkcert). +It's a tool that allows you to use `https` in local development seemingly via black magic. +The first time you run it, `mkcert` generates a root CA and adds it to the system and browser's trusted roots. +Afterwards, you can freely generate new leaf certificates on demand (without root) that are automatically trusted. + +Custom roots are often used in enterprise and VPN software. +When you install the VPN, or as part of a managed IT solution, it'll include some root CAs for you to trust. + +Side note, I highly recommend using private CAs and mTLS for service-to-service connections. +It's about as `dank` as one can get when designing distributed systems. +There are [cloud offerings](https://aws.amazon.com/private-ca/) available, and unlike public CAs, they actually give you the private key so it works for QUIC. + + +## Certificate Hashes +WebRTC also uses TLS (technically DTLS) even when establishing a peer-to-peer connection. +How does this work? + +Both peers generate a ECDSA certificate (fast compared to RSA) and compute its hash. +They then send the hash as part of the SDP exchange to some secure middle-man (usually a HTTPS server). +Yes, you do use a server even when establishing a peer-to-peer connection unless you're a freak who exchanges TLS certificates via USB drive. + +The peers draw straws and one of them assumes the role of the ~bottom~ server for the TLS handshake. +Both sides transmit a TLS certificate (mTLS) and verify that the hash matches the exchanged hash. +Ta-da, connection established. + +The same approach can be used for QUIC both peer-to-peer and client-to-server. +We're effectively just trusting individual certificates (by hash) instead of a root CA. +Just like root CAs, you **need** to secure the transfer of trusted certificates otherwise you're vulnerable to MITM attacks. + +Here's some rustls configuration that [validates certificates based on hash](https://github.com/kixelated/web-transport-rs/blob/3e656ca4e89c60c6c3b45fda6e4c67db7c9b2ec2/web-transport-quinn/src/client.rs#L232). +It's not the prettiest code but it works. + + +# WebTransport +Unfortunately, Chrome's implementation of WebTransport leaves a lot to be desired. +Rant incoming, grab some popcorn. + +**NOTE**: Firefox is spared from this rant because I haven't tested it. +Safari is spared because they haven't implemented WebTransport yet... + + +## Disable Certificate Validation +There's a Chrome flag that apparently lets you disable certificate validation for WebTransport: [chrome://flags/#webtransport-developer-mode](chrome://flags/#webtransport-developer-mode) +>When enabled, removes the requirement that all certificates used for WebTransport over HTTP/3 are issued by a known certificate root. – Mac, Windows, Linux, ChromeOS, Android + +If the description is to be trusted, this would mean disabling certificate validation on every website (that uses WebTransport) which is just a horrific thought. +This is the equivalent to silently disabling `https` on every website via a benign developer flag. +I sincerely hope that the description is just wrong and this only applies to `localhost` or something; somebody should test it. + +If you're having trouble with the TLS handshake then absolutely turn it on and **don't forget to turn it off afterwards**. +Not many sites use WebTransport right now but it would be super awkward when they do. + + +## Custom Roots +Chrome currently doesn't support custom root CAs for WebTransport. +I've reported the issue multiple times to the WebTransport developer but it's apparently by design? + +It's quite baffling, because you can use custom roots for HTTP/3 but not WebTransport... which uses HTTP/3. +There's literally no reason why it should use different certificate validation logic. +Just call the same function! + +This is an unfortunate "bug" as it rules out tools like `mkcert`. +Local development and private networks need to use another approach. + + +## serverCertificateHashes +There was this awkward "Certificate Hashes" section earlier talking about WebRTC. +That's because WebTransport supports [providing a list of certificate hashes](https://developer.mozilla.org/en-US/docs/Web/API/WebTransport/WebTransport#servercertificatehashes) for a similar approach. + +Unfortunately, there are some strings attached. +The certificates MUST be valid for less than 14 days and MUST use ECDSA. +Apparently 2 weeks is the sweet spot between "secure" and "annoying as fuck". + +These are good restrictions so you can't be lazy and ship the hash of some long-lived certificate with your application. +However it means we absolutely need to figure out how to rotate certificates because 14 days is not a lot of time. +Additionally, we need a secure mechanism to transmit our certificate hashes otherwise we're the major of MITM town. + +So in practice, you have to run a TLS web server, often on the same port, just to distribute TLS certificates for WebTransport. +It makes the "feature" quite frustrating in practice and make connection establishment more expensive (time and cpu). + +## Local Development +So what's the best approach if you want to use WebTransport in development? +Unfortunately, I think `serverCertificateHashes` is the best (right now) as it doesn't require browser configuration. + +`moq-relay` listens on TCP and UDP. +- The server [generates a TLS certificate](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/server.rs#L241) on startup +- The TCP socket is used to host a web server with a [/certificate.sha256](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-relay/src/web.rs#L44) route. +- The UDP socket is used to host a QUIC/WebTransport server. + +The client has to first establish a HTTP connection to [fetch the certificate hash](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/js/moq/src/lite/connection.ts#L225). +Then it can [connect to the WebTransport server](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/js/moq/src/lite/connection.ts#L231) using the provided hash. + +This only works on `localhost` because it uses an insecure HTTP fetch to get the certificate hashes. +We also don't care about certificate rotation because you can just restart the localhost server after 14 days + +But if you want to use WebTransport on a private network, oof. +You'll need to configure the web server to serve HTTPS using your own root CA just to deliver ephemeral certificate hashes. +You can even return the hash of *exact same TLS certificate* used to establish the HTTPS connection, but now WebTransport will work. +And of course, you'll need a mechanism to rotate these certificates as they're only valid for 14 days tops. + + +# Finished +TLS is not too bad in production once you realize it's all about proving that you own a domain. +There's a lot of existing tooling and resources out there. + +But it's a pain in the butt for non-public servers, as the whole "chain of trust" thing doesn't work any longer. +WebTransport makes life even more difficult. +Please Mr Google, add support for root CAs already, it should be like a single line of code to reuse the same CAs as HTTP. + +Want to commiserate about TLS pain? +Join the [MoQ Discord](https://discord.gg/FCYF3p99mr) or even the [rustls Discord](https://discord.gg/MCSB76RU96). + +Written by [@kixelated](https://github.com/kixelated). +![@kixelated](/blog/avatar.png) From 67a3ac60e7d91e47a34cfe8cdc61f0235165a5f3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Jul 2025 12:35:26 -0700 Subject: [PATCH 2/2] More changes to the TLS blog. --- public/blog/tls-and-quic/warning.png | Bin 0 -> 52256 bytes src/pages/blog/tls-and-quic.mdx | 142 +++++++++++++++------------ 2 files changed, 78 insertions(+), 64 deletions(-) create mode 100644 public/blog/tls-and-quic/warning.png diff --git a/public/blog/tls-and-quic/warning.png b/public/blog/tls-and-quic/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..e98740d99eebd4532697e6c2c16cabe65aa76024 GIT binary patch literal 52256 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 G$G#B*VG~>l$uw$M_d@l}9T+)-@0%Y7# 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�n zUCyWCUyT3tu>z6a*7Zl%CZU;DIWorVSz76i`-VyZL~P8}`PJ*_Tp3kKfS12NlZpD{ zwZEx;@SSeryA@qa#cII^hf#3C=#X&+R>~n^H4SS*2EOa~H&(`e^v<{x#X@DIG2+ue zFlfbVb~FmC3PAZ;{Pj~YLAOVvHlN_$;IV1W~3WR;Zh|sJP3Q0CnLMmenUusM9-Zk36d^G z)9WI4ev?dYp-+9$B%8`TG?wC8m&#|~;=L;N1P(_aj;V8F&KC?04@@@2>|wio>|BiHT_TP9hzw-#?a?mybe!=65U^5RlQN zVE~C`GdeYQ_5bb=sIKGZ8{!sHi8uOwSyR;CJZ9;L-)b>xj=1YCvY%CN^9_>rw1KBD z?4$R=F89&jtaEdORv%I_y-%fUCgP~&OI$vX^;-t;GDF%>_bC10ghreV&nsPf3M@(O zg){LCabzL@MmhUg_!gw7Vq_&kWet{|9V=hJqjT)s%On3#`0>%Y?6H>QH{%a^WSSi= zm@9>sAaJ+QyqRb5XJ&7xFO86y#e@VR_JG;<0v#z-zVLrv@c)h=v_#MUU4u~B(EtCN#ODuQq literal 0 HcmV?d00001 diff --git a/src/pages/blog/tls-and-quic.mdx b/src/pages/blog/tls-and-quic.mdx index f4d53d0..284138c 100644 --- a/src/pages/blog/tls-and-quic.mdx +++ b/src/pages/blog/tls-and-quic.mdx @@ -1,9 +1,9 @@ --- layout: "@/layouts/global.astro" -title: TLS and QUIC: A Masochist's Guide +title: "TLS and QUIC: A Masochist's Guide" author: kixelated -description: Setting up TLS is a pain, but it's a requirement for HTTPS. QUIC and WebTransport introduce even more pain so it's time to be a masochist. -cover: "/blog/replacing-webrtc/artifact.png" +description: Setting up TLS is a pain, but it's a requirement for HTTPS. QUIC and WebTransport introduce even more pain. We like pain, right? +cover: "/blog/tls-and-quic/warning.png" date: 2025-07-28 --- @@ -13,22 +13,30 @@ I hope you're having a great day. The sun is about to shine brighter. I was inspired by Helios himself to write about the riveting topic of TLS and QUIC. -In my opinion, the most difficult part about QUIC is setting up a different protocol: QUIC *requires* TLS. -If you screw it up, you'll get a scary ERROR screen and users won't be able to connect. -This guide also applies to HTTPS and WebTransport... with some important distinctions. +In my opinion, the most difficult part about QUIC is setting up a different protocol. +QUIC *requires* TLS. +There's no way to disable encryption and only some clients let you circumvent certificate validation. +If you screw it up, you'll get a scary WARNING screen and users won't be able to connect. +
+ ![WARNING](/blog/tls-and-quic/warning.png) +
I'm sick, so this is the only art you're getting today.
+
-tl;dr -- TLS authenticates who is allowed to serve `example.com` via certificates. -- Root CAs issue certificates to those who can prove it via DNS. -- Cloud offerings don't support QUIC yet, so that rules out the easy options. -- TLS is hell for local development, _especially_ for WebTransport. +Most of this guide applies to HTTPS in general, which makes sense as HTTP/3 uses QUIC. +WebTransport too as it's layered on top of HTTP/3 but there are some important distinctions at the end... +## tl;dr +- **TLS authenticates** who is allowed to serve `example.com` via certificates. +- **Root CAs** issue certificates to those who can prove it via DNS. +- **Cloud providers** don't support (non-HTTP/3) QUIC yet, ruling out the easy options. +- TLS is annoying for **local development**, _especially_ for WebTransport. ## About Me I'm not a security engineer but I have dabbled in the low-level protocols. For some unbeknownst reason, I've implemented both DTLS 1.2 (for WebRTC) and TLS 1.3 (for QUIC)... in Go. -Both were undoubtedly insecure but somehow passed the security audit and served production traffic for weeks... before getting the `rm -rf` treatment. +Both were undoubtedly insecure but somehow passed the security audit and served production traffic at Twitch. +But then I left and those servers rightfully got the `rm -rf` treatment. When in doubt, always refer to the nerds who take security seriously and use the correct terminology. I understand a lot of the security primitives but I don't exactly have the LinkedIn Professional Certificates to back it up. @@ -45,23 +53,23 @@ TLS certificates can be used to sign other TLS certificates creating a "chain of Without TLS, an attacker could intercept your traffic and pretend to be `example.com` to harvest your credentials, called a "man-in-the-middle" (MITM) attack. Imagine if your router, ISP, or fellow coffee shop customer could pretend to be your bank. Bad times ahead. +The fact that the private key is (virtually) unguessable is the only reason why I haven't taken out a second mortgage in your name. QUIC requires [TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446). +It's good stuff, much better than TLS 1.2 in my opinion. There is no way to disable encryption but depending on the client, you can modify certificate validation. The IETF grey-beards decreed that the protocol can never be insecure, lest a lowly application developer shoot themselves in the foot. -- HTTP/3, WebTransport, and of course, Media over QUIC all use QUIC under the hood so all of this applies. +- HTTP/3, WebTransport, and of course, [Media over QUIC](https://quic.video) all use QUIC under the hood. - HTTP/2 *technically* does not require TLS but browsers require it as a forcing function. - HTTP/1 is the lone exception, allowing you to choose if you want to connect to `http://example.com` (insecure) or `https://example.com` (TLS). -Not only is TLS required when using newer protocols, it's often required when using newer browser APIs. -[Secure Context APIs](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts) require using TLS or a localhost connection. -Want to do literally anything? Then you need to use HTTPS. +But even if you choose to use an insecure `http://` connection, it prevents you from using [newer browser APIs](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts). +Want to notify a user when their Hot Pocket® has finished cooking? +Then you need to use HTTPS, lest your Hot Pocket® Tracker™ get compromised by a state actor. -So yeah, browser vendors don't want you to make a security oopsie whoopsie and thus, effectively mandate TLS -But there's very little reason to use an insecure connection anyway so suck it up and let some cloud provider handle TLS for you. - -...unless you're one of the early birds using QUIC... +So yeah, browser vendors don't want you to make a security oopsie whoopsie and thus, effectively mandate TLS. +Suck it up and let some cloud provider handle TLS for you... unless you're one of the early birds using QUIC... *ominous foreshadowing* @@ -70,18 +78,20 @@ But there's very little reason to use an insecure connection anyway so suck it u At a high level, a TLS connection is established via: 1. The client connects to `example.com` and sends a `ClientHello`. - - The client (usually) sends the domain via a [SNI extension](https://en.wikipedia.org/wiki/Server_Name_Indication). + - The client (usually) sends the domain via a [SNI extension](https://en.wikipedia.org/wiki/Server_Name_Indication). 2. The server transmits a `ServerHello` along with a TLS certificate signed for `example.com`. - - The server can use SNI to choose the certificate to serve. + - The server uses SNI to choose the certificate if it hosts multiple websites. 3. The client verifies that the TLS certificate is for `example.com`. -4. The client verifies that the TLS certificate was signed/created by a "root CA". + - It must have been signed by a "root CA", or a certificate signed by a "root CA", or a certificate signed by a certificate signed by a "root CA"... What is a root CA? Your browser and operating system ship with a list of trusted entities that are authorized to issue certificates. It's like having a list of approved locksmith companies that can make keys for any house. The list changes over time as these companies are audited or catastrophically compromised. -If you've ever had to `apt install ca-certificates` when setting up a Docker image, this is why. + +*Fun fact*, this is why you may have to explicitly `apt update && apt install ca-certificates` when setting up a Docker image. +Otherwise, if the base image contained certificate roots, it would get super stale. One of the interesting things about TLS is that the client can choose how to verify the provided TLS certificate. There's no requirement that you use these provided root CAs, or a root CA at all. @@ -95,7 +105,7 @@ If your server listens on `example.com`, then we need to prove to some root CA t The easiest option unfortunately doesn't work for QUIC. Poop. -Virtually every cloud provider offers HTTPS support. +Virtually every cloud provider offers HTTPS support often via their own root CA. You point your domain name at their load balancer and they procure a TLS certificate. But I glossed over something, there's actually two parts to a "TLS certificate": the private key and the certificate. @@ -103,9 +113,9 @@ None of these services actually give you, the customer, the private key. Instead, they run a HTTPS or TLS load balancer that terminates TLS and proxies unencrypted traffic to your backend. It's done for simplicity and security, they don't want you having access to the keys. -This won't work for QUIC until cloud providers start offering QUIC load balancers (one day). +This load balancing approach won't work for QUIC until cloud providers start offering QUIC load balancers (one day). Welcome to UDP protocols; they're barely supported on cloud platforms because TCP and HTTP is so widespread. -At least there's hope for QUIC support in the future *because* it powers HTTP/3. +At least there's hope for QUIC support in the future because it powers HTTP/3. ## LetsEncrypt @@ -118,21 +128,24 @@ How this works is that you need to somehow prove to LetsEncrypt that you own a s This could be in the form of an A record that points to an IP address you own or a TXT record with some token. There are a few different [challenge types](https://letsencrypt.org/docs/challenge-types/): -- *HTTP-01*: Host a HTTP endpoint (not using TLS) that returns a specified token on the specified path. You own the domain if it points to your HTTP server. -- *DNS-01*: Create a DNS record with a specified token. You own the domain if you can create this TXT record; no server required. -- *TLS-ALPN-01*: Host a TLS endpoint that returns a specified token during the TLS handshake. Same idea as HTTP-01, but often more convenient because no HTTP server is required. +- *HTTP-01*: Host a HTTP endpoint (insecure) that returns a specified token on the specified path. You own the domain if it points to your HTTP server. +- *DNS-01*: Create a DNS record with a specified token. You own the domain if you can create this TXT record. The server doesn't even have to be running. +- *TLS-ALPN-01*: Host a TLS endpoint that returns a specified token during the TLS handshake. Same idea as HTTP-01, but more convenient for TLS load balancers. This cert generation can be automated via [certbot](https://certbot.eff.org/) or any [ACME library](https://letsencrypt.org/docs/client-options/#libraries). -It's a little bit annoying because of the 90 day expiration; any long-lived servers will need to periodically restart or reload certificates +The main downside of LetsEncrypt is the 90 day expiration. +That means you actually have to be a good developer and worry about certificate rotations instead of punting it to future you. +That means adding a way to reload certificates, or periodically hitting the good old `restart` button. ## ACME It would be remiss of me to not mention that LetsEncrypt uses [ACME v2](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment). You don't need to use `certbot` and in fact you could integrate ACME directly into your workflow. -In fact, I'm using a **not recommended** [terraform module](https://github.com/vancluever/terraform-provider-acme) to generate certificates for `relay.quic.video`. -It's not recommended because if I forget to run terraform every so often, then oops, my certificate will expire and users can no longer connect to my server. +In fact, I'm using a [terraform module](https://github.com/vancluever/terraform-provider-acme) to generate certificates for `relay.quic.video`. +It's **not recommended** because if I forget to run terraform every so often, then oops, my certificate will expire and users can no longer connect to my server. +But it's easy and it works for now so I'm embracing my folly. -I'm also considering using a [ACME library](https://crates.io/crates/instant-acme) within `moq-relay` itself to provision a TLS certificate on startup and every 90 days. +I'd like to add [ACME support](https://crates.io/crates/instant-acme) within `moq-relay` itself to provision a TLS certificate on startup and automatically refresh it. This makes it a lot easier to use cloud providers as you don't need to fumble with background services (in Docker, ew) and reloading the server whenever a new certificate is generated. The downside is being unable to generate wildcard certificates as those require DNS challenges. @@ -144,7 +157,7 @@ The problem with a service like LetsEncrypt is that it requires our server to be What if we're running our own private network or just developing an application locally? If LetsEncrypt can't connect to our private network, then it can't give us a TLS certificate. -Additionally, LetsEncrypt requires a domain name and potentially a public IP. +Additionally, LetsEncrypt requires a domain name or a public IP. These aren't expensive but it's not something you can reasonably ask developers to purchase just to try running your code. Remember when I said the client was responsible for verifying a certificate however it sees fit? @@ -160,7 +173,7 @@ There's usually a **DANGER** warning associated and **DANGER** indeed. If you skip certificate validation, your connection will still be encrypted but now it's vulnerable to a MITM attack. The server still has to present a certificate, but the client will blindly accept *any* certificate. -Even if it was generated nanoseconds ago, was riddled with typos, and claimed to be `pornhub.com`: doesn't matter. +Even if it was generated nanoseconds ago, is riddled with typos, and claims to be `porhnub.com`: doesn't matter. `moq-relay` supports the [--tls-disable-verify](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/client.rs#L22) by [jumping through a few hoops with rustls](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/client.rs#L202). There's similar flags in most CLI tools, like curl's `--insecure` flag. @@ -168,12 +181,16 @@ There's similar flags in most CLI tools, like curl's `--insecure` flag. ## Custom Root CAs Instead of using a "trusted" root CA that ships with the browser or operating system, we can use our own. -We can trivially generate our *own* root CA which can then be used to sign our *own* certificates. +It's trivial to generate root CA which can then be used to sign certificates. No more depending on FAANG, we are our own auditor now. -This is arguably safer than using public roots because our own client can be configured to ONLY accept our certificates. +``` +openssl req -new -x509 -days 365 -nodes -out ca-cert.pem -keyout ca-key.pem -subj "/C=US/ST=CA/L=San Francisco/O=CoolCidsClub/OU=DaveLaptop/CN=cool.bro" +``` + +This is arguably safer than using public roots because our own client can be configured to ONLY accept our root CA. It doesn't matter if one of the many public CAs gets hacked as long as our root is kept under lock and key. -But note that you're now responsible for a lot more infrastructure, like the ability to revoke compromised certificates. +But note that you're now responsible for stuff like revoking certificates if you truly care about the securities. The catch is that we need to configure clients to trust our root CA. This normally requires admin/root access, and can be done at an operating system or browser level. @@ -187,22 +204,22 @@ Afterwards, you can freely generate new leaf certificates on demand (without roo Custom roots are often used in enterprise and VPN software. When you install the VPN, or as part of a managed IT solution, it'll include some root CAs for you to trust. -Side note, I highly recommend using private CAs and mTLS for service-to-service connections. +**Side note:** I highly recommend using private CAs and mTLS for service-to-service connections. It's about as `dank` as one can get when designing distributed systems. -There are [cloud offerings](https://aws.amazon.com/private-ca/) available, and unlike public CAs, they actually give you the private key so it works for QUIC. +There are cloud offerings available ([AWS](https://aws.amazon.com/private-ca/)) and they actually give you the private key, so it works for QUIC. ## Certificate Hashes WebRTC also uses TLS (technically DTLS) even when establishing a peer-to-peer connection. How does this work? -Both peers generate a ECDSA certificate (fast compared to RSA) and compute its hash. +Both peers generate a ECDSA certificate (or RSA I guess) and compute its SHA256 hash. They then send the hash as part of the SDP exchange to some secure middle-man (usually a HTTPS server). -Yes, you do use a server even when establishing a peer-to-peer connection unless you're a freak who exchanges TLS certificates via USB drive. +Yes, you do need a server even when establishing a peer-to-peer connection unless you're a freak who exchanges TLS certificates via USB drive. The peers draw straws and one of them assumes the role of the ~bottom~ server for the TLS handshake. Both sides transmit a TLS certificate (mTLS) and verify that the hash matches the exchanged hash. -Ta-da, connection established. +Ta-da, connection established, ignoring all of the ICE shenanigans. The same approach can be used for QUIC both peer-to-peer and client-to-server. We're effectively just trusting individual certificates (by hash) instead of a root CA. @@ -240,7 +257,7 @@ It's quite baffling, because you can use custom roots for HTTP/3 but not WebTran There's literally no reason why it should use different certificate validation logic. Just call the same function! -This is an unfortunate "bug" as it rules out tools like `mkcert`. +I classify this as a bug because it rules out tools like `mkcert`. Local development and private networks need to use another approach. @@ -256,28 +273,25 @@ These are good restrictions so you can't be lazy and ship the hash of some long- However it means we absolutely need to figure out how to rotate certificates because 14 days is not a lot of time. Additionally, we need a secure mechanism to transmit our certificate hashes otherwise we're the major of MITM town. -So in practice, you have to run a TLS web server, often on the same port, just to distribute TLS certificates for WebTransport. -It makes the "feature" quite frustrating in practice and make connection establishment more expensive (time and cpu). - -## Local Development -So what's the best approach if you want to use WebTransport in development? -Unfortunately, I think `serverCertificateHashes` is the best (right now) as it doesn't require browser configuration. +## Private Networks +So what's the best approach if you want to use WebTransport on localhost or private networks? +Unfortunately, I think `serverCertificateHashes` is the best (right now) as it doesn't require users to configure their browser and disable TLS... -`moq-relay` listens on TCP and UDP. -- The server [generates a TLS certificate](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/server.rs#L241) on startup -- The TCP socket is used to host a web server with a [/certificate.sha256](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-relay/src/web.rs#L44) route. -- The UDP socket is used to host a QUIC/WebTransport server. +`moq-relay` listens on TCP and UDP (:443 by default). +- The server [generates a TLS certificate](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-native/src/server.rs#L241) on startup. +- The client [fetches the certificate hash](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/js/moq/src/lite/connection.ts#L225) via a HTTP [/certificate.sha256](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/rs/moq-relay/src/web.rs#L44) endpoint. +- The client then [connects to the WebTransport server](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/js/moq/src/lite/connection.ts#L231) using the provided hash. -The client has to first establish a HTTP connection to [fetch the certificate hash](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/js/moq/src/lite/connection.ts#L225). -Then it can [connect to the WebTransport server](https://github.com/kixelated/moq/blob/becf50263488ded6bb26c4cbc3d1ffd14ab11f5b/js/moq/src/lite/connection.ts#L231) using the provided hash. +When connecting to `localhost`, the certificate fetch can use good old HTTP. +But if you want to use WebTransport to connect to any other private network, oof. +The web server will need to use HTTPS to serve the certificate hash. -This only works on `localhost` because it uses an insecure HTTP fetch to get the certificate hashes. -We also don't care about certificate rotation because you can just restart the localhost server after 14 days +What this means is that you're establishing a TLS connection just to establish another TLS connection. +In fact, you could use an **identical** certificate for both the HTTPS and WebTransport connections. +But now you have to deal with 14 day certificate rotations, all because Chrome doesn't support custom root CAs. -But if you want to use WebTransport on a private network, oof. -You'll need to configure the web server to serve HTTPS using your own root CA just to deliver ephemeral certificate hashes. -You can even return the hash of *exact same TLS certificate* used to establish the HTTPS connection, but now WebTransport will work. -And of course, you'll need a mechanism to rotate these certificates as they're only valid for 14 days tops. +It's not a major problem once you figure it out. +It's just frustrating. # Finished @@ -286,7 +300,7 @@ There's a lot of existing tooling and resources out there. But it's a pain in the butt for non-public servers, as the whole "chain of trust" thing doesn't work any longer. WebTransport makes life even more difficult. -Please Mr Google, add support for root CAs already, it should be like a single line of code to reuse the same CAs as HTTP. +Please Mr Google, add support for custom root CAs already, it should be like a single line of code to reuse the same CAs as HTTP. Want to commiserate about TLS pain? Join the [MoQ Discord](https://discord.gg/FCYF3p99mr) or even the [rustls Discord](https://discord.gg/MCSB76RU96).