csr-gen 0.2.0

Creates csrs ready for use with Lets Encrypt.
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#![cfg_attr(test, deny(warnings))]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
//! # csr_gen
//!
//! csr_gen provides a few easy to use methods to generate a csr for a given private key.
//! It is meant to be used as part of a pipeline for generating Let's Encrypt certificates
//! where the private key is stored somewhere not easily accessed.

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};

/// Configuration for a given private key listing what csrs to sign
/// with a list of domains to include.
#[derive(Deserialize, Debug)]
pub struct Config {
    /// The private key file name
    pub key: String,
    /// A mapping from names for the csrs to a list of domain names.
    /// The first domain name will be the subject.
    pub csrs: BTreeMap<String, Vec<String>>,
}

/// Generic error type for csr_gen
type Error = Box<std::error::Error>;

type Result<T> = std::result::Result<T, Error>;

impl Config {
    /// Create a config from a given string, returning a generic error
    /// if the string doesn't represent a valid Config
    pub fn from_str(contents: &str) -> Result<Self> {
        Ok(toml::from_str(contents)?)
    }

    /// Generates the csrs into the given path.
    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()?;

            // Subject name
            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)?;

            // Set public key
            req_builder.set_pubkey(key)?;

            // Set subjectAltName extension
            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)?;

            // Sign result
            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);
    }
}