Browse Source

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.
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
2fc4eef60c
  1. 1
      Cargo.toml
  2. 6
      README.md
  3. 19
      tacd/Cargo.toml
  4. 69
      tacd/src/certificate.rs
  5. 155
      tacd/src/main.rs
  6. 36
      tacd/src/server.rs

1
Cargo.toml

@ -1,6 +1,7 @@
[workspace] [workspace]
members = [ members = [
"acmed", "acmed",
"tacd",
] ]
[profile.release] [profile.release]

6
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. 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 ## Frequently Asked Questions

19
tacd/Cargo.toml

@ -0,0 +1,19 @@
[package]
name = "tacd"
version = "0.1.0"
authors = ["Rodolphe Breard <rodolphe@what.tf>"]
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"

69
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<Private>,
public_key: &PKey<Public>,
acme_ext: &str,
) -> Result<X509, Error> {
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(&not_before)?;
let not_after = Asn1Time::days_from_now(CRT_NB_DAYS_VALIDITY)?;
builder.set_not_after(&not_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<Private>, X509), Error> {
let (priv_key, pub_key) = gen::p256()?;
let cert = get_certificate(domain, &priv_key, &pub_key, acme_ext)?;
Ok((priv_key, cert))
}

155
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<String, Error> {
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<String, Error> {
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);
}
};
}

36
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<Private>,
) -> 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())
}
Loading…
Cancel
Save