#![cfg_attr(test, deny(warnings))]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
extern crate openssl;
#[macro_use]
extern crate serde_derive;
extern crate tempdir;
extern crate toml;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use openssl::hash::MessageDigest;
use openssl::pkey::PKeyRef;
use openssl::stack::Stack;
use openssl::x509::{X509Extension, X509Name, X509Req};
#[derive(Deserialize, Debug)]
pub struct Config {
pub key: String,
pub csrs: BTreeMap<String, Vec<String>>,
}
type Error = Box<std::error::Error>;
type Result<T> = std::result::Result<T, Error>;
impl Config {
pub fn from_str(contents: &str) -> Result<Self> {
Ok(toml::from_str(contents)?)
}
pub fn generate_csrs<P: AsRef<Path>>(&self, path: P, key: &PKeyRef) -> Result<()> {
let path = path.as_ref();
for (file, domains) in self.csrs.iter() {
let mut req_builder = X509Req::builder()?;
let mut subj_builder = X509Name::builder()?;
subj_builder.append_entry_by_text("CN", &domains[0])?;
let subj = subj_builder.build();
req_builder.set_subject_name(&subj)?;
req_builder.set_pubkey(key)?;
let ext_stack = {
let context = req_builder.x509v3_context(None);
let mut san_str: String = Default::default();
for domain in domains {
san_str.push_str("DNS:");
san_str.push_str(&domain);
san_str.push_str(",");
}
let new_len = san_str.len() - 1;
san_str.truncate(new_len);
let san = X509Extension::new(None, Some(&context), "subjectAltName", &san_str)?;
let mut ext_stack = Stack::new()?;
ext_stack.push(san)?;
ext_stack
};
req_builder.add_extensions(&ext_stack)?;
req_builder.sign(key, MessageDigest::sha512())?;
let req = req_builder.build();
let mut f = File::create(path.join(file)).unwrap();
f.write_all(&req.to_pem()?)?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use openssl::pkey::PKey;
use tempdir::TempDir;
const CFG: &'static str = include_str!("../config.toml");
const KEY: &'static [u8] = include_bytes!("../key");
const CSR1: &'static str = include_str!("../csr1.csr");
const CSR2: &'static str = include_str!("../dom2.csr");
#[test]
fn test_config_parse() {
let cfg = Config::from_str(CFG).unwrap();
assert_eq!(cfg.key, "key");
assert_eq!(cfg.csrs,
[("csr1.csr", vec!["dom1.ca", "a.dom1.ca"]),
("dom2.csr", vec!["dom2.com", "b.dom2.com"])]
.into_iter()
.map(|&(k, ref v)| {
(k.to_owned(), v.into_iter().map(|&v| v.to_owned()).collect())
})
.collect());
}
#[test]
fn test_generate_csrs() {
let dir = TempDir::new("csr_gen").unwrap();
let cfg = Config::from_str(CFG).unwrap();
let key = PKey::private_key_from_pem(KEY).unwrap();
cfg.generate_csrs(&dir, &key).unwrap();
let mut contents = Default::default();
let mut f = File::open(dir.path().join("csr1.csr")).unwrap();
f.read_to_string(&mut contents).unwrap();
assert_eq!(contents, CSR1);
contents.clear();
let mut f = File::open(dir.path().join("dom2.csr")).unwrap();
f.read_to_string(&mut contents).unwrap();
assert_eq!(contents, CSR2);
}
}