From 2fc4eef60c28637482f6e319ee855252bd0d95e9 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Fri, 26 Apr 2019 17:42:43 +0200 Subject: [PATCH] Add tacd, a daemon for the tls-alpn-01 challenge ACMEd should and will remain as simple as possible and let the user alone take care of the challenge validation. However, this philosophy does not forbid the project itself to distribute additional tools that are designed to improve the user experience. Because the TLS-ALPN ecosystem is currently very slim, adding tacd is really benefic to ACMEd. --- Cargo.toml | 1 + README.md | 6 +- tacd/Cargo.toml | 19 +++++ tacd/src/certificate.rs | 69 ++++++++++++++++++ tacd/src/main.rs | 155 ++++++++++++++++++++++++++++++++++++++++ tacd/src/server.rs | 36 ++++++++++ 6 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 tacd/Cargo.toml create mode 100644 tacd/src/certificate.rs create mode 100644 tacd/src/main.rs create mode 100644 tacd/src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 34b2972..d112f5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "acmed", + "tacd", ] [profile.release] diff --git a/README.md b/README.md index eba6bc0..b3c715e 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,12 @@ In order to compile ADMEd, you will need the [Rust](https://www.rust-lang.org/) ACMEd depends on the OpenSSL. The minimal supported versions are those from the [openssl](https://docs.rs/openssl/) crate, currently OpenSSL 1.0.1 through 1.1.1 and LibreSSL 2.5 through 2.8. ``` -cargo build --release && strip target/release/acmed +cargo build --release +strip target/release/acmed +strip target/release/tacd ``` -The executable is located in `target/release/acmed`. +The executable are located in the `target/release` directory. ## Frequently Asked Questions diff --git a/tacd/Cargo.toml b/tacd/Cargo.toml new file mode 100644 index 0000000..3d9a78c --- /dev/null +++ b/tacd/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tacd" +version = "0.1.0" +authors = ["Rodolphe Breard "] +edition = "2018" +description = "TLS-ALPN Challenge Daemon" +keywords = ["acme", "tls", "alpn", "X.509"] +repository = "https://github.com/breard-r/acmed" +readme = "../README.md" +license = "MIT OR Apache-2.0" +include = ["src/**/*", "Cargo.toml", "LICENSE-*.txt"] + +[dependencies] +acme_common = { path = "../acme_common" } +clap = "2.32" +env_logger = "0.6" +log = "0.4" +openssl = "0.10" +syslog = "4.0" diff --git a/tacd/src/certificate.rs b/tacd/src/certificate.rs new file mode 100644 index 0000000..47db759 --- /dev/null +++ b/tacd/src/certificate.rs @@ -0,0 +1,69 @@ +use acme_common::error::Error; +use acme_common::gen; +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private, Public}; +use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; +use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509}; + +const X509_VERSION: i32 = 0x02; +const CRT_SERIAL_NB_BITS: i32 = 32; +const CRT_NB_DAYS_VALIDITY: u32 = 7; +const INVALID_EXT_MSG: &str = "Invalid acmeIdentifier extension."; + +fn get_certificate( + domain: &str, + private_key: &PKey, + public_key: &PKey, + acme_ext: &str, +) -> Result { + let mut x509_name = X509NameBuilder::new()?; + x509_name.append_entry_by_text("O", crate::APP_ORG)?; + let ca_name = format!("{} TLS-ALPN-01 Authority", crate::APP_NAME); + x509_name.append_entry_by_text("CN", &ca_name)?; + let x509_name = x509_name.build(); + + let mut builder = X509Builder::new()?; + builder.set_version(X509_VERSION)?; + let serial_number = { + let mut serial = BigNum::new()?; + serial.rand(CRT_SERIAL_NB_BITS - 1, MsbOption::MAYBE_ZERO, false)?; + serial.to_asn1_integer()? + }; + builder.set_serial_number(&serial_number)?; + builder.set_subject_name(&x509_name)?; + builder.set_issuer_name(&x509_name)?; + builder.set_pubkey(public_key)?; + let not_before = Asn1Time::days_from_now(0)?; + builder.set_not_before(¬_before)?; + let not_after = Asn1Time::days_from_now(CRT_NB_DAYS_VALIDITY)?; + builder.set_not_after(¬_after)?; + + builder.append_extension(BasicConstraints::new().build()?)?; + let ctx = builder.x509v3_context(None, None); + let san_ext = SubjectAlternativeName::new().dns(domain).build(&ctx)?; + builder.append_extension(san_ext)?; + + let ctx = builder.x509v3_context(None, None); + let mut v: Vec<&str> = acme_ext.split('=').collect(); + let value = v.pop().ok_or_else(|| Error::from(INVALID_EXT_MSG))?; + let acme_ext_name = v.pop().ok_or_else(|| Error::from(INVALID_EXT_MSG))?; + if !v.is_empty() { + return Err(Error::from(INVALID_EXT_MSG)); + } + let acme_ext = X509Extension::new(None, Some(&ctx), &acme_ext_name, &value) + .map_err(|_| Error::from(INVALID_EXT_MSG))?; + builder + .append_extension(acme_ext) + .map_err(|_| Error::from(INVALID_EXT_MSG))?; + builder.sign(private_key, MessageDigest::sha256())?; + let cert = builder.build(); + Ok(cert) +} + +pub fn gen_certificate(domain: &str, acme_ext: &str) -> Result<(PKey, X509), Error> { + let (priv_key, pub_key) = gen::p256()?; + let cert = get_certificate(domain, &priv_key, &pub_key, acme_ext)?; + Ok((priv_key, cert)) +} diff --git a/tacd/src/main.rs b/tacd/src/main.rs new file mode 100644 index 0000000..70a7fc5 --- /dev/null +++ b/tacd/src/main.rs @@ -0,0 +1,155 @@ +use acme_common::error::Error; +use clap::{App, Arg, ArgMatches}; +use log::{debug, error, info}; +use std::fs::File; +use std::io::{self, Read}; + +mod certificate; +mod server; + +const APP_NAME: &str = env!("CARGO_PKG_NAME"); +const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); +const APP_ORG: &str = "ACMEd"; +const DEFAULT_PID_FILE: &str = "/var/run/admed.pid"; +const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:5001"; +const ALPN_ACME_PROTO_NAME: &[u8] = b"\x0aacme-tls/1"; + +fn read_line(path: Option<&str>) -> Result { + let mut input = String::new(); + match path { + Some(p) => File::open(p)?.read_to_string(&mut input)?, + None => io::stdin().read_line(&mut input)?, + }; + let line = input.trim().to_string(); + Ok(line) +} + +fn get_acme_value(cnf: &ArgMatches, opt: &str, opt_file: &str) -> Result { + match cnf.value_of(opt) { + Some(v) => Ok(v.to_string()), + None => { + debug!( + "Reading {} from {}", + opt, + cnf.value_of(opt_file).unwrap_or("stdin") + ); + read_line(cnf.value_of(opt_file)) + } + } +} + +fn init(cnf: &ArgMatches) -> Result<(), Error> { + acme_common::init_server( + cnf.is_present("foregroung"), + cnf.value_of("pid-file").unwrap_or(DEFAULT_PID_FILE), + ); + let domain = get_acme_value(cnf, "domain", "domain-file")?; + let ext = get_acme_value(cnf, "acme-ext", "acme-ext-file")?; + let listen_addr = cnf.value_of("listen").unwrap_or(DEFAULT_LISTEN_ADDR); + let (pk, cert) = certificate::gen_certificate(&domain, &ext)?; + info!("Starting {} on {} for {}", APP_NAME, listen_addr, domain); + server::start(listen_addr, &cert, &pk)?; + Ok(()) +} + +fn main() { + let matches = App::new(APP_NAME) + .version(APP_VERSION) + .arg( + Arg::with_name("listen") + .long("listen") + .short("l") + .help("Specifies the host and port to listen on") + .takes_value(true) + .value_name("host:port"), + ) + .arg( + Arg::with_name("domain") + .long("domain") + .short("d") + .help("The domain that is being validated") + .takes_value(true) + .value_name("STRING") + .conflicts_with("domain-file") + ) + .arg( + Arg::with_name("domain-file") + .long("domain-file") + .help("File from which is read the domain that is being validated") + .takes_value(true) + .value_name("FILE") + .conflicts_with("domain") + ) + .arg( + Arg::with_name("acme-ext") + .long("acme-ext") + .short("e") + .help("The acmeIdentifier extension to set in the self-signed certificate") + .takes_value(true) + .value_name("STRING") + .conflicts_with("acme-ext-file") + ) + .arg( + Arg::with_name("acme-ext-file") + .long("acme-ext-file") + .help("File from which is read the acmeIdentifier extension to set in the self-signed certificate") + .takes_value(true) + .value_name("FILE") + .conflicts_with("acme-ext-file") + ) + .arg( + Arg::with_name("log-level") + .long("log-level") + .help("Specify the log level") + .takes_value(true) + .value_name("LEVEL") + .possible_values(&["error", "warn", "info", "debug", "trace"]), + ) + .arg( + Arg::with_name("to-syslog") + .long("log-syslog") + .help("Sends log messages via syslog") + .conflicts_with("to-stderr"), + ) + .arg( + Arg::with_name("to-stderr") + .long("log-stderr") + .help("Prints log messages to the standard error output") + .conflicts_with("log-syslog"), + ) + .arg( + Arg::with_name("foregroung") + .long("foregroung") + .short("f") + .help("Runs in the foregroung"), + ) + .arg( + Arg::with_name("pid-file") + .long("pid-file") + .help("Specifies the location of the PID file") + .takes_value(true) + .value_name("FILE") + .conflicts_with("foregroung"), + ) + .get_matches(); + + match acme_common::logs::set_log_system( + matches.value_of("log-level"), + matches.is_present("log-syslog"), + matches.is_present("to-stderr"), + ) { + Ok(_) => {} + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(2); + } + }; + + match init(&matches) { + Ok(_) => {} + Err(e) => { + error!("{}", e); + std::process::exit(1); + } + }; +} diff --git a/tacd/src/server.rs b/tacd/src/server.rs new file mode 100644 index 0000000..86840da --- /dev/null +++ b/tacd/src/server.rs @@ -0,0 +1,36 @@ +use acme_common::error::Error; +use log::debug; +use openssl::pkey::{PKey, Private}; +use openssl::ssl::{self, SslAcceptor, SslMethod}; +use openssl::x509::X509; +use std::net::TcpListener; +use std::sync::Arc; +use std::thread; + +pub fn start( + listen_addr: &str, + certificate: &X509, + private_key: &PKey, +) -> Result<(), Error> { + let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; + acceptor.set_alpn_select_callback(|_, client| { + debug!("ALPN negociation"); + ssl::select_next_proto(crate::ALPN_ACME_PROTO_NAME, client) + .ok_or(ssl::AlpnError::ALERT_FATAL) + }); + acceptor.set_private_key(private_key)?; + acceptor.set_certificate(certificate)?; + acceptor.check_private_key()?; + let acceptor = Arc::new(acceptor.build()); + let listener = TcpListener::bind(listen_addr)?; + for stream in listener.incoming() { + if let Ok(stream) = stream { + let acceptor = acceptor.clone(); + thread::spawn(move || { + debug!("New client"); + let _ = acceptor.accept(stream).unwrap(); + }); + }; + } + Err("Main thread loop unexpectedly exited".into()) +}