w3c / webtransport

WebTransport is a web API for flexible data transport
https://w3c.github.io/webtransport/
Other
845 stars 50 forks source link

Confirm behaviour of server certificates. #508

Closed hamishwillee closed 8 months ago

hamishwillee commented 1 year ago

I'm updating WebTransport docs for MDN: https://github.com/mdn/content/pull/26529/files#r1182080186. The constructor option serverCertificateHashes documentation doesn't make much sense to me and I'm also finding https://w3c.github.io/webtransport/#certificate-hashes a little opaque.

I (think) understand Web PKI:

  1. As part of TLS handshake client says Hello to server
  2. Server responds with server Hello containing a certificate.
  3. Certificate contains public key and is signed with some trusted root.
  4. If browser is able to authenticate the certificate has been signed by a trusted source (i.e. it has been signed by a certificate that the browser trusts) it check's the server name matches the intended target. Sends back a secret encrypted with the public key of the server, and the two can now communicate (yes, this last bit is wishy washy, not relevant to the discussion I don't think(.

For server certificates I think it works this way:

a. Client code "somehow" has SHA-256 hash(es) of server certificate(s) (or whatever other algorithms get added later to the spec). b. Client constructs WebTransport passing the hash/algorithms

  1. (as before) Browser says hello to server in client TLS hello.
  2. (as before) Server responds with server Hello containing a certificate.
  3. Browser gets the certificate. Instead of validating against the CA certificate it creates a SHA256 of the certificate. If that matches a hash supplied in the original WebTransport constructor, the browser trusts the certificate - and "normal TLS" then proceeds.

Is that correct? If not, would appreciate your advice on how this actually works.

How is it envisaged that the client code gets the hash(s) in the first place? It feels like a chicken and egg problem - you won't be able to download code with the latest server hashes until you have already got a TLS connection. In which case the server must have had a certificate that you can trust at some point.

hamishwillee commented 1 year ago

Also, is there any test code or example code for this. given the value is an ArrayBuffer, I'm sure it is not as easy as:

const transport = new WebTransport(url, {
  serverCertificateHashes: [
    {
      algorithm: "sha-256",
      value: "5a155927eba7996228455e4721e6fe5f739ae15db6915d765e5db302b4f8a274",
    }
  ],
});
martinthomson commented 1 year ago

This is an opportunity to improve the documentation in the spec, but this might help.

You need to think of TLS as providing a secure connection to the holder of a certificate. But TLS doesn't care beyond that, so you could be connecting to the wrong server.

In PKI, we look for a chain of signatures that connect to a trusted root. This is more or less as you describe (the key exchange part not so much, but that's TLS business). That tells the client that it is good for some name. The client also needs to check if it has the expected name.

So we have three conditions before the client considers the connection to be good:

  1. TLS is OK.
  2. The certificate is OK (for use in identifying servers by name).
  3. The name is the right one.

With a hash, the client is told directly which certificate(s) is/are good. No need for the second and third checks. Instead, the client checks if the hash of the certificate is equal to one that it was given.

The hash allows the instruction to the client to be a little smaller. You don't need to supply a whole certificate of hundreds of bytes, just a small set of bytes. Collision resistance ensures that if the hash matches, it is absurdly unlikely that certificate is a different one.

Your sample code should be sufficient (assuming that someone has a certificate with that hash, of course).

hamishwillee commented 1 year ago

Thanks @martinthomson

With a hash, the client is told directly which certificate(s) is/are good. No need for the second and third checks. Instead, the client checks if the hash of the certificate is equal to one that it was given.

I understand that given a hash and a matching certificate (from server) - the client knows it has a secure connection to a server that holds the certificate (that it knows about).

But to confirm, what is the client here? My assumption above is that it is the user agent, which is checking against hashes supplied in the constructor call to WebTransport?

[My assumption is based on thinking that there is no reason to specify the hash in the constructor to WebTransport unless something else is doing the checking. Unless this is somehow to tell the server which certificate you want it to send back. Hence the confused questions about the message flow]

Or to put it another way, I'm interested in the perspective of a javascript developer using the API. I "think" they never see a certificate. They magically get a hash from somewhere and all the work of then setting up the connection is handled by the browser.

The follow on question then is "where" they magically get the hash from. I mean, would it be that you have to connect to the app every 2 weeks to make sure you download a new set of hashes before the old ones disappear?

Your sample code should be sufficient (assuming that someone has a certificate with that hash, of course).

Thank you. So there is implicit conversion from a string to an ArrayBuffer?

martinthomson commented 1 year ago

implicit conversion from a string to an ArrayBuffer?

Oh, probably not as it happens. My mistake. WebRTC accepts strings, but it looks like this one needs conversion.

vasilvv commented 1 year ago

How is it envisaged that the client code gets the hash(s) in the first place? It feels like a chicken and egg problem - you won't be able to download code with the latest server hashes until you have already got a TLS connection. In which case the server must have had a certificate that you can trust at some point.

Typically, there is a third-party that would act as a trusted intermediary between you.

Imagine, for instance, you have a cloud provider that can provision VMs for you. You can ask the provider to launch a virtual machine for you; the cloud provider would create a VM with a WebTransport-based SSH/RDP-like server for its certificate; the server would generate a certificate automatically, and then the cloud provider (having trusted connectivity to the VM) would connect to the server, get its certificate and report its hash via the cloud console. This means that the client now can connect directly to the VM in question via the web UI despite it not having a long-lived TLS certificate (most existing solutions require a Web-to-SSH proxy or something similar).

hamishwillee commented 1 year ago

Thank you very much. I feel my questions have been answered. I'll leave this open in case you decide the specification requires a little more clarification.

jan-ivar commented 9 months ago

Adding an example might be nice.