kixelated / web-transport-rs

Rust WebTransport library for native and WASM
https://quic.video
Apache License 2.0
100 stars 14 forks source link

Example for creating a self-signed certificate with `rcgen` #13

Closed ameba23 closed 9 months ago

ameba23 commented 9 months ago

I would like to be able to generate a self-signed certificate programmatically from rust, rather than using openssl as in the given example here.

The rcgen crate provides a way of doing this, which works fine when using Quinn directly with a rust server and client. But using this crate together with the web client using the example here (with Chromium), i get an 'unknown certificate' error when using a self signed certificate generated with the code below.

I'm pretty sure there is no issue with the chosen signature algorithm, and i have checked that the method of hashing the certificate gives the same results as with https://github.com/kixelated/webtransport-rs/blob/3153f826549bfa04a220e89c8798f37b822dc39f/webtransport-quinn/cert/generate#L12

So there must be some other issue with the certificate parameters / extensions that make it different from one generated with https://github.com/kixelated/webtransport-rs/blob/3153f826549bfa04a220e89c8798f37b822dc39f/webtransport-quinn/cert/generate#L9

Note that these rcgen certificates do work fine with the rust client example - so it is an issue relating to web / serverCertificateHashes.

This is maybe an rcgen issue rather than a webtransport-quinn issue.

use ring::digest::{Context, Digest, SHA256};

fn main() {  
    let mut cert_params =
        rcgen::CertificateParams::new(vec!["localhost".to_string(), "127.0.0.1".to_string()]);
    cert_params.not_after = rcgen::date_time_ymd(2024, 02, 06);
    cert_params.not_before = rcgen::date_time_ymd(2024, 01, 29);

    cert_params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;

    let cert = rcgen::Certificate::from_params(cert_params).unwrap();

    println!("{}", cert.serialize_pem().unwrap());
    println!("{}", cert.serialize_private_key_pem());

    // Print certificate hash
    let cert_der = cert.serialize_der().unwrap();
    let mut context = Context::new(&SHA256);
    context.update(&cert_der);
    let digest = context.finish();
    println!("{:?}", digest);
}
kixelated commented 9 months ago

I would love a better certificate setup and to get rid of mkcert. I wish you the best of luck. Here's what I've learned:

  1. Chrome does NOT allow self-signed root CAs for WebTransport. I really really really wish they would fix it.
  2. Instead we use the fingerprint method, in which WebTransport will allow an ephemeral that is valid for 10 days.
  3. The sha256 fingerprint computation is actually not trivial. Here's some Rust code to do it.
  4. Unfortunately we need to transfer this cert to the client, either via the filesystem (like the example) or a HTTPS endpoint (like moq-rs/moq-js).

Check if your code produces the same fingerprint as mine. That might just be the issue; it was a pain in the butt figuring out what needs to be hashed.

ameba23 commented 9 months ago

Thank you @kixelated the problem was indeed with the fingerprints. Using your method you linked to gave me a different hash to what i was getting, and using that hash let me successfully make a connection using Chrome.

Using rc-gen::Certificate::serialize_der appears to give different output than rustls_pemfile::certs gives - both are der encoding of the same length - but not exactly the same. For the record - rustls_pemfile::certs is the one which works.

ameba23 commented 9 months ago

Here is a working example which writes files compatible with the echo example in this repo:

use ring::digest::{digest, SHA256};
use std::{fs::File, io::Write, time::Duration};
use time::OffsetDateTime;

fn main() {
    let mut cert_params =
        rcgen::CertificateParams::new(vec!["localhost".to_string(), "127.0.0.1".to_string()]);

    let now = OffsetDateTime::now_utc();
    cert_params.not_before = now;
    cert_params.not_after = now + Duration::from_secs(60 * 60 * 24 * 10); // 10 days

    cert_params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;

    let cert = rcgen::Certificate::from_params(cert_params).unwrap();

    let cert_pem = cert.serialize_pem().unwrap();
    println!("{}", cert_pem);
    let mut file = File::create("localhost.crt").unwrap();
    file.write_all(cert_pem.as_bytes()).unwrap();

    let priv_pem = cert.serialize_private_key_pem();
    println!("{}", priv_pem);
    let mut file = File::create("localhost.key").unwrap();
    file.write_all(priv_pem.as_bytes()).unwrap();

    let mut cert_reader = std::io::BufReader::new(cert_pem.as_bytes());
    let certs = rustls_pemfile::certs(&mut cert_reader).unwrap();
    let cert_der = certs.first().expect("No certificate found");
    let fingerprint = digest(&SHA256, cert_der.as_ref());
    let fingerprint_hex = hex::encode(fingerprint.as_ref());

    println!("Fingerprint {}", fingerprint_hex);
    let mut file = File::create("localhost.hex").unwrap();
    file.write_all(fingerprint_hex.as_bytes()).unwrap();
}