diff --git a/Cargo.lock b/Cargo.lock index cbaa1bc..da1d452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1236,12 +1236,11 @@ dependencies = [ [[package]] name = "ratpack" version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a13f3016fde9fbad50dd0f7a2cef95b421224908dbe68125a4f5b447bdecb7" dependencies = [ "async-recursion", "http", "hyper", + "log", "tokio", "tokio-rustls", "webpki", diff --git a/Cargo.toml b/Cargo.toml index 2009ba9..7a6b4bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ base64 = "^0.13" serde_json = "^1.0" serde = "^1.0" tokio = { version = "^1.16", features = ["full"] } -ratpack = "^0.1" # { git = "https://github.com/zerotier/ratpack", branch = "main" } hyper = "^0.14" http = "^0.2" url = { version = "^2.2", features = [ "serde" ] } deadpool-postgres = { version = "^0.10", features = ["serde"] } +ratpack = { version = "^0.1", path = "../ratpack", features = ["logging"] } log = "^0.4" trust-dns-client = "^0.20" openssl = "^0.10" @@ -35,18 +35,26 @@ futures = "^0.3" futures-core = "^0.3" chrono = { version = "^0.4", features = [ "serde" ] } x509-parser = { version = "^0.12", features = [ "ring", "verify", "validate" ] } +rustls = { version = "^0.20", optional = true } +rustls-pemfile = { version = "^0.3", optional = true } +webpki-roots = { version = "^0.22", optional = true } [lib] +[[example]] +name = "acmed-tls" +path = "examples/acmed-tls.rs" +required-features = ["tls"] + [[example]] name = "acmed" path = "examples/acmed.rs" +[features] +tls = ["rustls", "rustls-pemfile", "webpki-roots", "ratpack/tls"] + [dev-dependencies] env_logger = "^0.9" -rustls = "^0.20" -rustls-pemfile = "^0.3" -webpki-roots = "^0.22" eggshell = "^0.1" # { path = "../eggshell" } bollard = "^0.11" tempfile = "^3.3" diff --git a/examples/acmed-tls.rs b/examples/acmed-tls.rs new file mode 100644 index 0000000..5fb30d3 --- /dev/null +++ b/examples/acmed-tls.rs @@ -0,0 +1,146 @@ +use std::{ + io::Write, + ops::Add, + time::{Duration, SystemTime}, +}; + +use openssl::{ + error::ErrorStack, + pkey::{PKey, Private}, + rsa::Rsa, + x509::{X509Extension, X509Name, X509Req}, +}; + +use coyote::{ + acme::{ + ca::{CACollector, CA}, + challenge::Challenger, + handlers::{configure_routes, ServiceState}, + PostgresNonceValidator, + }, + models::Postgres, +}; + +use ratpack::prelude::*; + +const CHALLENGE_EXPIRATION: i64 = 600; + +#[tokio::main] +async fn main() -> Result<(), ServerError> { + // set HOSTNAME in your environment to something your webserver or certbot can hit; otherwise + // it will be 'localhost'. a cert will be generated with this name to serve the service with. + // This is really important. + let dnsname = &std::env::var("HOSTNAME").unwrap_or("localhost".to_string()); + + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + // + // to start a database to work with me: + // + // make postgres + // + let pg = Postgres::new("host=localhost dbname=coyote user=postgres", 10) + .await + .unwrap(); + pg.migrate().await.unwrap(); + + let c = Challenger::new(Some(chrono::Duration::seconds(CHALLENGE_EXPIRATION))); + let ca = CACollector::new(Duration::MAX); + + let pg2 = pg.clone(); + let c2 = c.clone(); + + // FIXME probably need something magical with signals here to manage shutdown that I don't want to think about yet + tokio::spawn(async move { + loop { + // FIXME whitelist all challenge requests. This is not how ACME is supposed to work. You have to write this. + c2.tick(|_c| Some(())).await; + // NOTE this will explode violently if it unwraps to error, e.g. if the db goes down. + c2.reconcile(pg2.clone()).await.unwrap(); + + tokio::time::sleep(Duration::new(1, 0)).await; + } + }); + + let mut ca2 = ca.clone(); + let (csr, key) = generate_csr(dnsname)?; + + let test_ca = CA::new_test_ca().unwrap(); + let cert = test_ca.generate_and_sign_cert( + csr, + SystemTime::now(), + SystemTime::now().add(Duration::from_secs(365 * 24 * 60 * 60)), + )?; + + let test_ca2 = test_ca.clone(); + + tokio::spawn(async move { + // after CA generation, write out the key and certificate + let mut buf = std::fs::File::create("ca.key").unwrap(); + let private = test_ca + .clone() + .private_key() + .private_key_to_pem_pkcs8() + .unwrap(); + buf.write(&private).unwrap(); + + let mut buf = std::fs::File::create("ca.pem").unwrap(); + let cert = test_ca.clone().certificate().to_pem().unwrap(); + buf.write(&cert).unwrap(); + + ca2.spawn_collector(|| -> Result { Ok(test_ca.clone()) }) + .await + }); + + let validator = PostgresNonceValidator::new(pg.clone()); + let ss = ServiceState::new( + format!("https://{}:8000", dnsname), + pg.clone(), + c, + ca, + validator, + )?; + let mut app = App::with_state(ss); + + configure_routes(&mut app, None); + + let key = key.private_key_to_der()?; + + let config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert( + vec![ + rustls::Certificate(cert.to_der()?), + rustls::Certificate(test_ca2.certificate().to_der()?), + ], + rustls::PrivateKey(key), + )?; + + Ok(app.serve_tls("0.0.0.0:8000", config).await?) +} + +fn generate_csr(dnsname: &str) -> Result<(X509Req, Rsa), ErrorStack> { + log::info!("hostname: {}", dnsname); + let mut namebuilder = X509Name::builder().unwrap(); + namebuilder.append_entry_by_text("CN", dnsname).unwrap(); + let mut req = X509Req::builder().unwrap(); + req.set_subject_name(&namebuilder.build()).unwrap(); + let mut extensions = openssl::stack::Stack::new()?; + extensions.push(X509Extension::new( + None, + Some(&req.x509v3_context(None)), + "subjectAltName", + &format!("DNS:{}", dnsname), + )?)?; + req.add_extensions(&extensions)?; + req.set_version(2)?; + + let key = Rsa::generate(4096).unwrap(); + // FIXME there has to be a much better way of doing this! + let pubkey = PKey::public_key_from_pem(&key.public_key_to_pem().unwrap()).unwrap(); + + req.set_pubkey(&pubkey).unwrap(); + Ok((req.build(), key)) +} diff --git a/examples/acmed.rs b/examples/acmed.rs index 5fb30d3..ace4cb1 100644 --- a/examples/acmed.rs +++ b/examples/acmed.rs @@ -1,15 +1,6 @@ -use std::{ - io::Write, - ops::Add, - time::{Duration, SystemTime}, -}; +use std::time::Duration; -use openssl::{ - error::ErrorStack, - pkey::{PKey, Private}, - rsa::Rsa, - x509::{X509Extension, X509Name, X509Req}, -}; +use openssl::error::ErrorStack; use coyote::{ acme::{ @@ -27,11 +18,6 @@ const CHALLENGE_EXPIRATION: i64 = 600; #[tokio::main] async fn main() -> Result<(), ServerError> { - // set HOSTNAME in your environment to something your webserver or certbot can hit; otherwise - // it will be 'localhost'. a cert will be generated with this name to serve the service with. - // This is really important. - let dnsname = &std::env::var("HOSTNAME").unwrap_or("localhost".to_string()); - env_logger::builder() .filter_level(log::LevelFilter::Info) .init(); @@ -64,38 +50,16 @@ async fn main() -> Result<(), ServerError> { }); let mut ca2 = ca.clone(); - let (csr, key) = generate_csr(dnsname)?; - let test_ca = CA::new_test_ca().unwrap(); - let cert = test_ca.generate_and_sign_cert( - csr, - SystemTime::now(), - SystemTime::now().add(Duration::from_secs(365 * 24 * 60 * 60)), - )?; - - let test_ca2 = test_ca.clone(); tokio::spawn(async move { - // after CA generation, write out the key and certificate - let mut buf = std::fs::File::create("ca.key").unwrap(); - let private = test_ca - .clone() - .private_key() - .private_key_to_pem_pkcs8() - .unwrap(); - buf.write(&private).unwrap(); - - let mut buf = std::fs::File::create("ca.pem").unwrap(); - let cert = test_ca.clone().certificate().to_pem().unwrap(); - buf.write(&cert).unwrap(); - ca2.spawn_collector(|| -> Result { Ok(test_ca.clone()) }) .await }); let validator = PostgresNonceValidator::new(pg.clone()); let ss = ServiceState::new( - format!("https://{}:8000", dnsname), + "http://127.0.0.1:8000".to_string(), pg.clone(), c, ca, @@ -105,42 +69,5 @@ async fn main() -> Result<(), ServerError> { configure_routes(&mut app, None); - let key = key.private_key_to_der()?; - - let config = rustls::ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert( - vec![ - rustls::Certificate(cert.to_der()?), - rustls::Certificate(test_ca2.certificate().to_der()?), - ], - rustls::PrivateKey(key), - )?; - - Ok(app.serve_tls("0.0.0.0:8000", config).await?) -} - -fn generate_csr(dnsname: &str) -> Result<(X509Req, Rsa), ErrorStack> { - log::info!("hostname: {}", dnsname); - let mut namebuilder = X509Name::builder().unwrap(); - namebuilder.append_entry_by_text("CN", dnsname).unwrap(); - let mut req = X509Req::builder().unwrap(); - req.set_subject_name(&namebuilder.build()).unwrap(); - let mut extensions = openssl::stack::Stack::new()?; - extensions.push(X509Extension::new( - None, - Some(&req.x509v3_context(None)), - "subjectAltName", - &format!("DNS:{}", dnsname), - )?)?; - req.add_extensions(&extensions)?; - req.set_version(2)?; - - let key = Rsa::generate(4096).unwrap(); - // FIXME there has to be a much better way of doing this! - let pubkey = PKey::public_key_from_pem(&key.public_key_to_pem().unwrap()).unwrap(); - - req.set_pubkey(&pubkey).unwrap(); - Ok((req.build(), key)) + Ok(app.serve("127.0.0.1:8000").await?) }