From 183c9242206dfa20dab2444a2e951a415c74a01e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 6 Feb 2024 02:50:23 +0000 Subject: [PATCH] feat: add cert-watch example --- Cargo.lock | 39 +++++++++- Cargo.toml | 3 + https-tls/cert-watch/Cargo.toml | 18 +++++ https-tls/cert-watch/README.md | 38 +++++++++ https-tls/cert-watch/cert.pem | 25 ++++++ https-tls/cert-watch/key.pem | 28 +++++++ https-tls/cert-watch/src/main.rs | 129 +++++++++++++++++++++++++++++++ 7 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 https-tls/cert-watch/Cargo.toml create mode 100644 https-tls/cert-watch/README.md create mode 100644 https-tls/cert-watch/cert.pem create mode 100644 https-tls/cert-watch/key.pem create mode 100644 https-tls/cert-watch/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ee6c50dc..5515e46d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1975,6 +1975,24 @@ dependencies = [ "libc", ] +[[package]] +name = "cert-watch" +version = "1.0.0" +dependencies = [ + "actix-files", + "actix-web", + "color-eyre", + "env_logger", + "eyre", + "futures-util", + "log", + "notify 6.1.1", + "parking_lot 0.12.1", + "rustls 0.21.10", + "rustls-pemfile", + "tokio 1.35.1", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -4765,7 +4783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d073fe2671399bed11572e89b1349d2ea54205a65282e7dc0b1e954c82ad597" dependencies = [ "minijinja", - "notify", + "notify 5.2.0", ] [[package]] @@ -5120,6 +5138,25 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.9", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-bigint" version = "0.4.4" diff --git a/Cargo.toml b/Cargo.toml index 6dfb9965..9128275f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "http-proxy", "https-tls/acme-letsencrypt", "https-tls/awc-https", + "https-tls/cert-watch", "https-tls/openssl", "https-tls/rustls-client-cert", "https-tls/rustls", @@ -98,6 +99,8 @@ chrono = { version = "0.4.20", default-features = false, features = ["clock", "s derive_more = "0.99.7" dotenvy = "0.15" env_logger = "0.11" +eyre = { version = "0.6", default-features = false, features = ["auto-install", "track-caller"] } +color-eyre = "0.6" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } log = "0.4" openssl = { version = "0.10.60", features = ["v110"] } diff --git a/https-tls/cert-watch/Cargo.toml b/https-tls/cert-watch/Cargo.toml new file mode 100644 index 00000000..f3e24600 --- /dev/null +++ b/https-tls/cert-watch/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cert-watch" +version = "1.0.0" +edition = "2021" + +[dependencies] +actix-web = { workspace = true, features = ["rustls-0_21"] } +actix-files.workspace = true +color-eyre.workspace = true +env_logger.workspace = true +eyre.workspace = true +futures-util.workspace = true +log.workspace = true +notify = "6" +rustls.workspace = true +rustls-pemfile = "1" +tokio = { workspace = true, features = ["time", "rt-multi-thread", "macros"] } +parking_lot = "0.12" diff --git a/https-tls/cert-watch/README.md b/https-tls/cert-watch/README.md new file mode 100644 index 00000000..f2452e9e --- /dev/null +++ b/https-tls/cert-watch/README.md @@ -0,0 +1,38 @@ +# HTTPS Server With TLS Cert/Key File Watcher + +## Usage + +### Certificate + +We put the self-signed certificate in this directory as an example but your browser would complain that it isn't secure. So we recommend to use [`mkcert`] to trust it. To use local CA, you should run: + +```sh +mkcert -install +``` + +If you want to generate your own cert/private key file, then run: + +```sh +mkcert -key-file key.pem -cert-file cert.pem 127.0.0.1 localhost +``` + +### Running The Example Server + +```console +$ cd https-tls/cert-watch +$ cargo run +starting HTTPS server at https://localhost:8443 +``` + +Reload the server by modifying the certificate metadata: + +```console +$ touch cert.pem +``` + +### Client + +- cURL: `curl -v --insecure https://127.0.0.1:8443` +- Browser: go to + +[`mkcert`]: https://github.com/FiloSottile/mkcert diff --git a/https-tls/cert-watch/cert.pem b/https-tls/cert-watch/cert.pem new file mode 100644 index 00000000..7c13d03e --- /dev/null +++ b/https-tls/cert-watch/cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEIjCCAoqgAwIBAgIRANtMOCE9la5aBBJkaPCckAUwDQYJKoZIhvcNAQELBQAw +aTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMR8wHQYDVQQLDBZyb2JA +c29tYnJhLmxvY2FsIChSb2IpMSYwJAYDVQQDDB1ta2NlcnQgcm9iQHNvbWJyYS5s +b2NhbCAoUm9iKTAeFw0yNDAyMDYwMTUzNTBaFw0yNjA1MDYwMDUzNTBaMEoxJzAl +BgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEfMB0GA1UECwwW +cm9iQHNvbWJyYS5sb2NhbCAoUm9iKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJ+H+iCey/ET+cq3DxCMjkNaUS8M+LqWaEqGBGGGly+mRGfJ30BvaULf +JIR1V0NkT38t9X/Y/7Qnv8ONa2WTnGNGfgrp+DQGGQBkfi2ggaARd8XoY8WlZBVg +UzfZ/9tNqJKx41M2tYunL59ucmB7osmmU8m74oKqylA1TkYEKzyqvkg0AJ3RVslH +/vmq2LKidbofXQnZWvs2PaePR2q7XXv7+bBaTvLeUIU4Vjww6o2kJTN4//kp2dtH +iwfcUxTT6B22X512AXxojp6+X6B1Y1XhHqQcPuZUZ6ITOcS3zCXje/ijDXoiPn3o +mQTM9UeD3zTlq2vZbSeRT1TQzZzRVPUCAwEAAaNkMGIwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFBf9LZZrFz95p+wnPmJz +Pavuiq9QMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF +AAOCAYEAf3tXixdMuwjPVzrR8TImg7Oe9UcYIHxUN27HospX0gpvKRLuFmxNNhN+ +aK0l765FaLH7acOY129BxAGzuG54+XQdeRcJH/SrNCeHfe5VCHc2+f/Fj6yjUrug +ZRlsbS57LFPgOaPFyej2bR4EDbpwGczc/ghCN4PBRGXC4onFGT7TsaAw3GaXnlMx +ToLuSnnZID9gcl6Wp0qR4baXJh5HrcxNOPIiO3k4U+70jcEty5mDSac0+mNsxgOp +O0w1S/YsQ/W+tG2lsb0huPw2XaAPu+GkkX1KmAV5PSVssIqmRcy6eqv4CkqKq1ur +WeKexHVO8VRQqwx8H/DZDnU1IxNx+khxxNSy16wo4LWU6BHfnEyOSW/S1DlsRUub +JtFg5dIkFBUmIBnqAUVvzIcwNvBZ5KvQ//mwVrsxWCdfre0o8lSaesc7GLMeaeht +jgP8h1ThjvilGfVHK0Skvnck46Ll2fPO5ZgNXUXlR8+1J3jYXPOYLWPgVLbMaLkQ +fQB2xLiv +-----END CERTIFICATE----- diff --git a/https-tls/cert-watch/key.pem b/https-tls/cert-watch/key.pem new file mode 100644 index 00000000..69613299 --- /dev/null +++ b/https-tls/cert-watch/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCfh/ognsvxE/nK +tw8QjI5DWlEvDPi6lmhKhgRhhpcvpkRnyd9Ab2lC3ySEdVdDZE9/LfV/2P+0J7/D +jWtlk5xjRn4K6fg0BhkAZH4toIGgEXfF6GPFpWQVYFM32f/bTaiSseNTNrWLpy+f +bnJge6LJplPJu+KCqspQNU5GBCs8qr5INACd0VbJR/75qtiyonW6H10J2Vr7Nj2n +j0dqu117+/mwWk7y3lCFOFY8MOqNpCUzeP/5KdnbR4sH3FMU0+gdtl+ddgF8aI6e +vl+gdWNV4R6kHD7mVGeiEznEt8wl43v4ow16Ij596JkEzPVHg9805atr2W0nkU9U +0M2c0VT1AgMBAAECggEAHBTDYpqRK45omdY/QJp9MD3lrHKMFcwD75pHiyM12Z1a +zSorshvqW2sL8oT1J1ew5qIgZLC90ehtSO7LyMWC8bam2ST2G7I3FGqcC0wFhTeN +7bhKV7AVPe4Gt/4Xm3LACZJmgW9P5ZU4PMgkOfeJYBV3CjuYU4fctOGtNYXmVncw +bVaNSbiekAPmChjDikFBbtp7tB+Qx7+kbpgSFrFraf1MRaHTJqE5z557Ttp7ttQ2 +D9XciwYzSh1LsBqS6aYTyEMX/ttGENW6Y1evRRUI1biOS50E7bnTp/vuIUAs75PR +w0Vdcr9s1rcJs8Ht2aiteGyWv9ErGLSiTyJxX3aaAQKBgQDT+gg8Gr6D+e3NAxt6 +2B85dXDA8Ziy3U5Tj46nTmsyK6C3i7eK8S3RG8dBWv76Z8jRmgWfvEsBVMwOCOTA +/Jnr7cJGRKV0oU5LVt/XdY3Yb9Ja0PrjHXP+DCLVz9gHzUkQ/RMyxhoT9UP5k0e4 +vvYhq/cMKtSpmt0NESx0ovqevQKBgQDAqZ9IO+4RduMdaftbp/zDMIi9JX6vDkYX +Oc+clmB0jwJak9AEy59x5phAQaS+aPcXJiVXslXUQfE9f1xXambHrMhrrZodzsTM +fRUnTquVT9Xdptn6nTtHGr3V/n+V1qIzFJz5xMoKFjoYR7hUa6YU2xVXnHLyMuEb +epI6ZfSumQKBgHK+4j5G6+JdJFDZ4cI5w41C+Wo4XcRU79Vj3IDMflKGM1WoGA7q +RzbuponGTEgYbiioC2tQbfmmgV8HiWy+UEPaTFPlTPs5Zjx6JmlnhQUoYuIuReEz +TFq1DxZWkEaI5YiAtifB/NPY7JbpFuX22R2ZDP5VIRE+d3JfXYU1xByxAoGAZWym +ert5/92rgH+boMvVx9fUFGcZwwRrp6x6fD+59YKXxaFNAElF3gt8GU+1b7wIYDpn +rDwo7P3uBub2cNqF0xZFyFHy8UA54ED1EuVadNc7il1dIY8Gds9AItjAx9vfNa7j +WKXdiuPan4+aHW8yVoZjPOUSqihY00N6mZ206vkCgYBz4yTW/UqyVf90l7c9Pdf0 +h7zmBIMTs0xiFc4jYtLNaueY6XJ2kCIAEC/m2qO7P/dz4nbTI1P0KYwrFrRZU1pw +NgsnWucXbZHfAyAKoOZnPy+V63MSxO9aYInYIpytIqZheRPv19B8c0g77jQi3isK +8gqCLEZjtxty24ihkq/WKg== +-----END PRIVATE KEY----- diff --git a/https-tls/cert-watch/src/main.rs b/https-tls/cert-watch/src/main.rs new file mode 100644 index 00000000..a686bfe4 --- /dev/null +++ b/https-tls/cert-watch/src/main.rs @@ -0,0 +1,129 @@ +use std::{fs::File, io::BufReader, path::Path}; + +use actix_web::{ + http::header::ContentType, middleware, web, App, HttpRequest, HttpResponse, HttpServer, +}; +use log::debug; +use notify::{Event, RecursiveMode, Watcher as _}; +use rustls::{Certificate, PrivateKey, ServerConfig}; +use rustls_pemfile::{certs, pkcs8_private_keys}; +use tokio::sync::mpsc; + +#[derive(Debug)] +struct TlsUpdated; + +async fn index(req: HttpRequest) -> HttpResponse { + debug!("{req:?}"); + + HttpResponse::Ok().content_type(ContentType::html()).body( + "\ +

Welcome to your TLS-secured homepage!

\ + ", + ) +} + +#[tokio::main(worker_threads = 2)] +async fn main() -> eyre::Result<()> { + color_eyre::install()?; + env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + + // signal channel used to notify main event loop of cert/key file changes + let (reload_tx, mut reload_rx) = mpsc::channel(1); + + let mut file_watcher = + notify::recommended_watcher(move |res: notify::Result| match res { + Ok(ev) => { + log::info!("files changed: {:?}", ev.paths); + reload_tx.blocking_send(TlsUpdated).unwrap(); + } + Err(err) => { + log::error!("file watch error: {err}"); + } + }) + .unwrap(); + + file_watcher + .watch(Path::new("cert.pem"), RecursiveMode::NonRecursive) + .unwrap(); + file_watcher + .watch(Path::new("key.pem"), RecursiveMode::NonRecursive) + .unwrap(); + + // start HTTP server reload loop + // + // loop reloads on TLS changes and exits on normal ctrl-c (etc.) signals + loop { + // load TLS cert/key files and + let config = load_rustls_config(); + + log::info!("starting HTTPS server at https://localhost:8443"); + + let mut server = HttpServer::new(|| { + App::new() + .service(web::resource("/").to(index)) + .wrap(middleware::Logger::default()) + }) + .workers(2) + .bind_rustls_021("127.0.0.1:8443", config)? + .run(); + + let server_hnd = server.handle(); + + tokio::select! { + // poll server continuously + res = &mut server => { + log::info!("server shut down via signal or manual command"); + res?; + break; + }, + + // receiving a message to reload the server + Some(_) = reload_rx.recv() => { + log::info!("TLS cert or key updated"); + + // send stop signal; no need to wait for completion signal here + // since we're about to await the server itself + drop(server_hnd.stop(true)); + + // poll and await server shutdown before + server.await?; + + // restart loop to reload cert/key files + continue; + } + } + } + + Ok(()) +} + +fn load_rustls_config() -> rustls::ServerConfig { + // init server config builder with safe defaults + let config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth(); + + // load TLS key/cert files + let cert_file = &mut BufReader::new(File::open("cert.pem").unwrap()); + let key_file = &mut BufReader::new(File::open("key.pem").unwrap()); + + // convert files to key/cert objects + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); + let mut keys: Vec = pkcs8_private_keys(key_file) + .unwrap() + .into_iter() + .map(PrivateKey) + .collect(); + + // exit if no keys could be parsed + if keys.is_empty() { + eprintln!("Could not locate PKCS 8 private keys."); + std::process::exit(1); + } + + config.with_single_cert(cert_chain, keys.remove(0)).unwrap() +}