mirror of https://github.com/breard-r/acmed.git
Browse Source
Add tacd, a daemon for the tls-alpn-01 challenge
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
6 changed files with 284 additions and 2 deletions
-
1Cargo.toml
-
6README.md
-
19tacd/Cargo.toml
-
69tacd/src/certificate.rs
-
155tacd/src/main.rs
-
36tacd/src/server.rs
@ -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" |
@ -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(¬_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<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))
|
||||
|
}
|
@ -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);
|
||||
|
}
|
||||
|
};
|
||||
|
}
|
@ -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())
|
||||
|
}
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue