Browse Source

Create an abstraction around the certificate

As for public and private keys, the certificate should also be
abstracted.

rel #2
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
ff1b6d1aa7
  1. 1
      acme_common/Cargo.toml
  2. 3
      acme_common/src/crypto.rs
  3. 163
      acme_common/src/crypto/openssl_certificate.rs
  4. 6
      acmed/src/acme_proto.rs
  5. 30
      acmed/src/acme_proto/certificate.rs
  6. 102
      acmed/src/certificate.rs
  7. 11
      acmed/src/storage.rs
  8. 68
      tacd/src/certificate.rs
  9. 5
      tacd/src/main.rs
  10. 11
      tacd/src/server.rs

1
acme_common/Cargo.toml

@ -22,6 +22,7 @@ openssl = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
syslog = "4.0"
time = "0.1"
toml = "0.5"
[target.'cfg(unix)'.dependencies]

3
acme_common/src/crypto.rs

@ -1,3 +1,6 @@
mod openssl_keys;
pub use openssl_keys::{gen_keypair, KeyType, PrivateKey, PublicKey};
pub const DEFAULT_ALGO: &str = "rsa2048";
mod openssl_certificate;
pub use openssl_certificate::{Csr, X509Certificate};

163
acme_common/src/crypto/openssl_certificate.rs

@ -0,0 +1,163 @@
use super::{gen_keypair, KeyType, PrivateKey, PublicKey};
use crate::b64_encode;
use crate::error::Error;
use openssl::asn1::Asn1Time;
use openssl::bn::{BigNum, MsbOption};
use openssl::hash::MessageDigest;
use openssl::stack::Stack;
use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509};
use std::collections::HashSet;
use time::strptime;
const APP_ORG: &str = "ACMEd";
const APP_NAME: &str = "ACMEd";
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.";
pub struct Csr {
inner_csr: X509Req,
}
impl Csr {
pub fn new(
pub_key: &PublicKey,
priv_key: &PrivateKey,
domains: &[String],
) -> Result<Self, Error> {
let mut builder = X509ReqBuilder::new()?;
builder.set_pubkey(&pub_key.inner_key)?;
let ctx = builder.x509v3_context(None);
let mut san = SubjectAlternativeName::new();
for dns in domains.iter() {
san.dns(&dns);
}
let san = san.build(&ctx)?;
let mut ext_stack = Stack::new()?;
ext_stack.push(san)?;
builder.add_extensions(&ext_stack)?;
builder.sign(&priv_key.inner_key, MessageDigest::sha256())?;
Ok(Csr {
inner_csr: builder.build(),
})
}
pub fn to_der_base64(&self) -> Result<String, Error> {
let csr = self.inner_csr.to_der()?;
let csr = b64_encode(&csr);
Ok(csr)
}
}
pub struct X509Certificate {
pub inner_cert: X509,
}
impl X509Certificate {
pub fn from_pem(pem_data: &[u8]) -> Result<Self, Error> {
Ok(X509Certificate {
inner_cert: X509::from_pem(pem_data)?,
})
}
pub fn from_acme_ext(domain: &str, acme_ext: &str) -> Result<(PrivateKey, Self), Error> {
let (pub_key, priv_key) = gen_keypair(KeyType::EcdsaP256)?;
let inner_cert = gen_certificate(domain, &pub_key, &priv_key, acme_ext)?;
let cert = X509Certificate { inner_cert };
Ok((priv_key, cert))
}
pub fn public_key(&self) -> Result<PublicKey, Error> {
let raw_key = self.inner_cert.public_key()?.public_key_to_pem()?;
let pub_key = PublicKey::from_pem(&raw_key)?;
Ok(pub_key)
}
// OpenSSL ASN1_TIME_print madness
// The real fix would be to add Asn1TimeRef access in the openssl crate.
//
// https://github.com/sfackler/rust-openssl/issues/687
// https://github.com/sfackler/rust-openssl/pull/673
pub fn not_after(&self) -> Result<time::Tm, Error> {
let formats = [
"%b %d %T %Y %Z",
"%b %d %T %Y %Z",
"%b %d %T %Y",
"%b %d %T %Y",
"%b %d %T.%f %Y %Z",
"%b %d %T.%f %Y %Z",
"%b %d %T.%f %Y",
"%b %d %T.%f %Y",
];
let not_after = self.inner_cert.not_after().to_string();
for fmt in formats.iter() {
if let Ok(t) = strptime(&not_after, fmt) {
return Ok(t);
}
}
Err(format!("invalid time string: {}", &not_after).into())
}
pub fn subject_alt_names(&self) -> HashSet<String> {
match self.inner_cert.subject_alt_names() {
Some(s) => s
.iter()
.filter(|v| v.dnsname().is_some())
.map(|v| v.dnsname().unwrap().to_string())
.collect(),
None => HashSet::new(),
}
}
}
fn gen_certificate(
domain: &str,
public_key: &PublicKey,
private_key: &PrivateKey,
acme_ext: &str,
) -> Result<X509, Error> {
let mut x509_name = X509NameBuilder::new()?;
x509_name.append_entry_by_text("O", APP_ORG)?;
let ca_name = format!("{} TLS-ALPN-01 Authority", 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.inner_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.inner_key, MessageDigest::sha256())?;
let cert = builder.build();
Ok(cert)
}

6
acmed/src/acme_proto.rs

@ -5,6 +5,7 @@ use crate::acme_proto::structs::{
};
use crate::certificate::Certificate;
use crate::storage;
use acme_common::crypto::Csr;
use acme_common::error::Error;
use std::fmt;
@ -176,8 +177,9 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String]) -> Result<
)?;
// 11. Finalize the order by sending the CSR
let (priv_key, pub_key) = certificate::get_key_pair(cert)?;
let csr = certificate::generate_csr(cert, &priv_key, &pub_key)?;
let (pub_key, priv_key) = certificate::get_key_pair(cert)?;
let domains: Vec<String> = cert.domains.iter().map(|e| e.dns.to_owned()).collect();
let csr = Csr::new(&pub_key, &priv_key, domains.as_slice())?.to_der_base64()?;
let data_builder = set_data_builder!(account, csr.as_bytes(), order.finalize);
let (order, nonce): (Order, String) =
http::get_obj(cert, root_certs, &order.finalize, &data_builder, &nonce)?;

30
acmed/src/acme_proto/certificate.rs

@ -1,13 +1,7 @@
use crate::certificate::{Algorithm, Certificate};
use crate::storage;
use acme_common::b64_encode;
use acme_common::crypto::{gen_keypair, KeyType, PrivateKey, PublicKey};
use acme_common::error::Error;
use openssl::hash::MessageDigest;
use openssl::stack::Stack;
use openssl::x509::extension::SubjectAlternativeName;
use openssl::x509::X509ReqBuilder;
use serde_json::json;
fn gen_key_pair(cert: &Certificate) -> Result<(PublicKey, PrivateKey), Error> {
let key_type = match cert.algo {
@ -37,27 +31,3 @@ pub fn get_key_pair(cert: &Certificate) -> Result<(PublicKey, PrivateKey), Error
gen_key_pair(cert)
}
}
pub fn generate_csr(
cert: &Certificate,
pub_key: &PublicKey,
priv_key: &PrivateKey,
) -> Result<String, Error> {
let mut builder = X509ReqBuilder::new()?;
builder.set_pubkey(&pub_key.inner_key)?;
let ctx = builder.x509v3_context(None);
let mut san = SubjectAlternativeName::new();
for c in cert.domains.iter() {
san.dns(&c.dns);
}
let san = san.build(&ctx)?;
let mut ext_stack = Stack::new()?;
ext_stack.push(san)?;
builder.add_extensions(&ext_stack)?;
builder.sign(&priv_key.inner_key, MessageDigest::sha256())?;
let csr = builder.build();
let csr = csr.to_der()?;
let csr = b64_encode(&csr);
let csr = json!({ "csr": csr });
Ok(csr.to_string())
}

102
acmed/src/certificate.rs

@ -2,13 +2,13 @@ use crate::acme_proto::Challenge;
use crate::config::{Account, Domain, HookType};
use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, PostOperationHookData};
use crate::storage::{certificate_files_exists, get_certificate};
use acme_common::crypto::X509Certificate;
use acme_common::error::Error;
use log::{debug, info, trace, warn};
use openssl::x509::X509;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::mpsc::SyncSender;
use time::{strptime, Duration};
use time::Duration;
#[derive(Clone, Debug)]
pub enum Algorithm {
@ -90,33 +90,6 @@ impl Certificate {
trace!("{}: {}", &self, msg);
}
// OpenSSL ASN1_TIME_print madness
// The real fix would be to add Asn1TimeRef access in the openssl crate.
//
// https://github.com/sfackler/rust-openssl/issues/687
// https://github.com/sfackler/rust-openssl/pull/673
fn parse_openssl_time_string(&self, time: &str) -> Result<time::Tm, Error> {
self.debug(&format!("Parsing OpenSSL time: \"{}\"", time));
let formats = [
"%b %d %T %Y %Z",
"%b %d %T %Y %Z",
"%b %d %T %Y",
"%b %d %T %Y",
"%b %d %T.%f %Y %Z",
"%b %d %T.%f %Y %Z",
"%b %d %T.%f %Y",
"%b %d %T.%f %Y",
];
for fmt in formats.iter() {
if let Ok(t) = strptime(time, fmt) {
self.trace(&format!("Format \"{}\" matches", fmt));
return Ok(t);
}
self.trace(&format!("Format \"{}\" does not match", fmt));
}
Err(format!("invalid time string: {}", time).into())
}
pub fn get_domain_challenge(&self, domain_name: &str) -> Result<Challenge, Error> {
let domain_name = domain_name.to_string();
for d in self.domains.iter() {
@ -128,9 +101,8 @@ impl Certificate {
Err(format!("{}: domain name not found", domain_name).into())
}
fn is_expiring(&self, cert: &X509) -> Result<bool, Error> {
let not_after = cert.not_after().to_string();
let not_after = self.parse_openssl_time_string(&not_after)?;
fn is_expiring(&self, cert: &X509Certificate) -> Result<bool, Error> {
let not_after = cert.not_after()?;
self.debug(&format!("not after: {}", not_after.asctime()));
// TODO: allow a custom duration (using time-parse ?)
let renewal_time = not_after - Duration::weeks(3);
@ -138,15 +110,8 @@ impl Certificate {
Ok(time::now_utc() > renewal_time)
}
fn has_missing_domains(&self, cert: &X509) -> bool {
let cert_names = match cert.subject_alt_names() {
Some(s) => s
.iter()
.filter(|v| v.dnsname().is_some())
.map(|v| v.dnsname().unwrap().to_string())
.collect(),
None => HashSet::new(),
};
fn has_missing_domains(&self, cert: &X509Certificate) -> bool {
let cert_names = cert.subject_alt_names();
let req_names = self
.domains
.iter()
@ -242,58 +207,3 @@ impl Certificate {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Algorithm, Certificate};
use std::collections::HashMap;
use std::sync::mpsc::sync_channel;
fn get_dummy_certificate() -> Certificate {
let (https_throttle, _) = sync_channel(0);
Certificate {
account: crate::config::Account {
name: String::new(),
email: String::new(),
},
domains: Vec::new(),
algo: Algorithm::Rsa2048,
kp_reuse: false,
remote_url: String::new(),
tos_agreed: false,
https_throttle,
hooks: Vec::new(),
account_directory: String::new(),
crt_directory: String::new(),
crt_name: String::new(),
crt_name_format: String::new(),
cert_file_mode: 0,
cert_file_owner: None,
cert_file_group: None,
pk_file_mode: 0,
pk_file_owner: None,
pk_file_group: None,
env: HashMap::new(),
id: 0,
}
}
#[test]
fn test_parse_openssl_time() {
let time_str_lst = [
"May 7 18:34:07 2024",
"May 7 18:34:07 2024 GMT",
"May 17 18:34:07 2024",
"May 17 18:34:07 2024 GMT",
"May 7 18:34:07.922661874 2024",
"May 7 18:34:07.922661874 2024 GMT",
"May 17 18:34:07.922661874 2024",
"May 17 18:34:07.922661874 2024 GMT",
];
let crt = get_dummy_certificate();
for time_str in time_str_lst.iter() {
let time_res = crt.parse_openssl_time_string(time_str);
assert!(time_res.is_ok());
}
}
}

11
acmed/src/storage.rs

@ -2,9 +2,8 @@ use crate::certificate::Certificate;
use crate::config::HookType;
use crate::hooks::{self, FileStorageHookData, HookEnvData};
use acme_common::b64_encode;
use acme_common::crypto::{PrivateKey, PublicKey};
use acme_common::crypto::{PrivateKey, PublicKey, X509Certificate};
use acme_common::error::Error;
use openssl::x509::X509;
use std::collections::HashMap;
use std::fmt;
use std::fs::{File, OpenOptions};
@ -212,15 +211,13 @@ pub fn set_priv_key(cert: &Certificate, key: &PrivateKey) -> Result<(), Error> {
}
pub fn get_pub_key(cert: &Certificate) -> Result<PublicKey, Error> {
let raw_key = get_certificate(cert)?.public_key()?.public_key_to_pem()?;
let pub_key = PublicKey::from_pem(&raw_key)?;
Ok(pub_key)
get_certificate(cert)?.public_key()
}
pub fn get_certificate(cert: &Certificate) -> Result<X509, Error> {
pub fn get_certificate(cert: &Certificate) -> Result<X509Certificate, Error> {
let path = get_file_path(cert, FileType::Certificate)?;
let raw_crt = read_file(cert, &path)?;
let crt = X509::from_pem(&raw_crt)?;
let crt = X509Certificate::from_pem(&raw_crt)?;
Ok(crt)
}

68
tacd/src/certificate.rs

@ -1,68 +0,0 @@
use acme_common::crypto::{gen_keypair, KeyType, PrivateKey, PublicKey};
use acme_common::error::Error;
use openssl::asn1::Asn1Time;
use openssl::bn::{BigNum, MsbOption};
use openssl::hash::MessageDigest;
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,
public_key: &PublicKey,
private_key: &PrivateKey,
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.inner_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.inner_key, MessageDigest::sha256())?;
let cert = builder.build();
Ok(cert)
}
pub fn gen_certificate(domain: &str, acme_ext: &str) -> Result<(PrivateKey, X509), Error> {
let (pub_key, priv_key) = gen_keypair(KeyType::EcdsaP256)?;
let cert = get_certificate(domain, &pub_key, &priv_key, acme_ext)?;
Ok((priv_key, cert))
}

5
tacd/src/main.rs

@ -1,15 +1,14 @@
use acme_common::crypto::X509Certificate;
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";
@ -46,7 +45,7 @@ fn init(cnf: &ArgMatches) -> Result<(), Error> {
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)?;
let (pk, cert) = X509Certificate::from_acme_ext(&domain, &ext)?;
info!("Starting {} on {} for {}", APP_NAME, listen_addr, domain);
server::start(listen_addr, &cert, &pk)?;
Ok(())

11
tacd/src/server.rs

@ -1,8 +1,7 @@
use acme_common::crypto::PrivateKey;
use acme_common::crypto::{PrivateKey, X509Certificate};
use acme_common::error::Error;
use log::debug;
use openssl::ssl::{self, AlpnError, SslAcceptor, SslMethod};
use openssl::x509::X509;
use std::net::TcpListener;
use std::sync::Arc;
use std::thread;
@ -30,14 +29,18 @@ macro_rules! listen_and_accept {
};
}
pub fn start(listen_addr: &str, certificate: &X509, private_key: &PrivateKey) -> Result<(), Error> {
pub fn start(
listen_addr: &str,
certificate: &X509Certificate,
private_key: &PrivateKey,
) -> 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(ALPN_ERROR)
});
acceptor.set_private_key(&private_key.inner_key)?;
acceptor.set_certificate(certificate)?;
acceptor.set_certificate(&certificate.inner_cert)?;
acceptor.check_private_key()?;
let acceptor = Arc::new(acceptor.build());
if cfg!(unix) && listen_addr.starts_with("unix:") {

Loading…
Cancel
Save