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()) +}