diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-03-28 17:36:41 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-03-28 17:39:19 -0400 |
commit | cf43dfb5cba67da6b1929add44ae1d79a230095c (patch) | |
tree | d32cad8343ddc34f556a0a84dc0331de0f4730bf | |
parent | f86d704b3c638867e78718add25a762c44a06962 (diff) |
save root key and certificate with password
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | Cargo.lock | 14 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/cli.rs | 31 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 99 | ||||
-rw-r--r-- | src/path.rs | 21 | ||||
-rw-r--r-- | src/pkey.rs | 44 | ||||
-rw-r--r-- | src/root.rs | 110 |
9 files changed, 230 insertions, 101 deletions
@@ -1,2 +1,8 @@ +# builds /target +# secrets .env +# certs +*.pem +*.crt +*.csr @@ -51,6 +51,7 @@ dependencies = [ "os_str_bytes", "strsim", "termcolor", + "terminal_size", "textwrap", ] @@ -337,10 +338,23 @@ dependencies = [ ] [[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "textwrap" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +dependencies = [ + "terminal_size", +] [[package]] name = "thiserror" @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "3.1.6", features = ["cargo", "derive", "env"] } +clap = { version = "3.1.6", features = ["cargo", "derive", "env", "wrap_help"] } openssl = "0.10.38" path-absolutize = "3.0.12" shellexpand = "2.1.0" @@ -14,46 +14,51 @@ pub enum Commands { } #[derive(Args, Debug)] +#[clap(about = "Generate a new root certificate")] pub struct Init { /// Base directory to store certificates #[clap(long, default_value = "~/.hancock", env = "CA_BASE_DIR")] pub base_dir: String, /// Algorithm to generate private keys ('RSA' or 'ECDSA') - #[clap(long, default_value = "RSA", validator = validate_key_type)] + #[clap(long, short = 't', default_value = "RSA", validator = validate_key_type)] pub key_type: String, /// Length to use when generating an RSA key. Ignored for ECDSA - #[clap(long, default_value_t = 4096)] + #[clap(long, short = 'b', default_value_t = 4096)] pub key_length: u32, /// Lifetime in days of the generated certificate - #[clap(long, default_value_t = 365 * 10)] + #[clap(long, short = 'd', default_value_t = 365 * 10)] pub lifetime: u32, - #[clap(long)] + /// Certificate CommonName + #[clap(long, short = 'n')] pub common_name: Option<String>, - #[clap(long)] + /// Certificate Country + #[clap(long, short = 'c')] pub country: Option<String>, - #[clap(long)] + /// Certificate State or Province + #[clap(long, short = 's')] pub state: Option<String>, - #[clap(long)] + /// Certificate Locality + #[clap(long, short = 'l')] pub locality: Option<String>, - #[clap(long)] + /// Certificate Organization + #[clap(long, short = 'o')] pub organization: Option<String>, - #[clap(long)] + /// Certificate Organizational Unit + #[clap(long, short = 'u')] pub organizational_unit: Option<String>, - #[clap(long, env = "CA_PASSWORD")] + /// Password for private key + #[clap(long, short = 'p', env = "CA_PASSWORD")] pub password: Option<String>, - - #[clap(long)] - pub no_password: bool, } fn validate_key_type(input: &str) -> Result<(), String> { @@ -1,6 +1,6 @@ -use std::str::FromStr; - pub mod path; +pub mod pkey; +pub mod root; #[derive(Clone, Copy)] pub enum KeyType { diff --git a/src/main.rs b/src/main.rs index f1f80b8..68d3b0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,11 @@ use clap::Parser; -use openssl::asn1::Asn1Time; -use openssl::bn::{BigNum, MsbOption}; -use openssl::ec::{EcGroup, EcKey}; -use openssl::hash::MessageDigest; -use openssl::nid::Nid; -use openssl::pkey::{Id, PKey, Private, self}; -use openssl::rsa::Rsa; -use openssl::x509::extension::{BasicConstraints, KeyUsage}; -use openssl::x509::*; - -use hancock::KeyType; use hancock::path; +use hancock::pkey; +use hancock::root; +use hancock::KeyType; use std::path::Path; -use std::fs::{read, write}; mod cli; use crate::cli::*; @@ -35,76 +26,28 @@ fn main() { let pkey_path = path::ca_pkey(&base_dir, key_type); let pkey = match Path::new(&pkey_path).exists() { - true => { - PKey::private_key_from_pem(&read(&pkey_path).unwrap()).unwrap() - }, + true => pkey::read_pkey(&pkey_path, args.password), false => { - let pkey = generate_pkey(key_type); - save_pkey(&pkey_path, &pkey); + let pkey = pkey::generate_pkey(key_type); + pkey::save_pkey(&pkey_path, &pkey, args.password); pkey } }; - } - } - // let key_type = KeyType::Ecdsa; - // let pkey = generate_pkey(key_type); - // let root_cert = generate_root_cert(pkey); -} - -fn generate_pkey(key_type: KeyType) -> PKey<Private> { - match key_type { - KeyType::Ecdsa => PKey::from_ec_key( - EcKey::generate(&EcGroup::from_curve_name(Nid::SECP384R1).unwrap()).unwrap(), - ) - .unwrap(), - KeyType::Rsa(bits) => PKey::from_rsa(Rsa::generate(bits).unwrap()).unwrap(), + let cert_path = path::ca_crt(&base_dir, key_type); + if !Path::new(&cert_path).exists() { + let cert = root::generate_root_cert( + args.lifetime, + &args.common_name, + &args.country, + &args.state, + &args.locality, + &args.organization, + &args.organizational_unit, + &pkey, + ); + root::save_root_cert(&cert_path, &cert); + } + } } } - -fn save_pkey(path: &str, key: &PKey<Private>) { - println!("{}", path); - path::ensure_dir(path); - write(path, key.private_key_to_pem_pkcs8().unwrap()).unwrap(); -} - -fn generate_root_cert(pkey: PKey<Private>) -> X509 { - let mut x509_name = X509Name::builder().unwrap(); - x509_name - .append_entry_by_nid(Nid::COMMONNAME, "ligma.dev") - .unwrap(); - let x509_name = x509_name.build(); - - let mut x509_builder = X509::builder().unwrap(); - x509_builder.set_version(2).unwrap(); - x509_builder.set_issuer_name(&x509_name).unwrap(); - x509_builder.set_subject_name(&x509_name).unwrap(); - - x509_builder - .set_not_before(&Asn1Time::days_from_now(0).unwrap()) - .unwrap(); - x509_builder - .set_not_after(&Asn1Time::days_from_now(365).unwrap()) - .unwrap(); - - x509_builder.set_pubkey(&pkey).unwrap(); - - let mut serial = BigNum::new().unwrap(); - serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap(); - x509_builder - .set_serial_number(&serial.to_asn1_integer().unwrap()) - .unwrap(); - - let basic_constraints = BasicConstraints::new().critical().ca().build().unwrap(); - x509_builder.append_extension(basic_constraints).unwrap(); - let key_usage = KeyUsage::new() - .digital_signature() - .key_encipherment() - .build() - .unwrap(); - x509_builder.append_extension(key_usage).unwrap(); - - x509_builder.sign(&pkey, MessageDigest::sha256()).unwrap(); - - x509_builder.build() -} diff --git a/src/path.rs b/src/path.rs index c7e5f29..f13c591 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,26 +1,33 @@ use crate::KeyType; use path_absolutize::*; use shellexpand; -use std::path::Path; use std::fs::create_dir_all; +use std::path::Path; pub fn ca_pkey(base_dir: &str, key_type: KeyType) -> String { format!("{}/authority.{}.pem", base_dir, key_type.to_string()) } +pub fn ca_crt(base_dir: &str, key_type: KeyType) -> String { + format!("{}/authority.{}.crt", base_dir, key_type.to_string()) +} pub fn base_dir(raw_base: &str) -> String { Path::new(&shellexpand::tilde(&raw_base).to_string()) - .absolutize() - .unwrap() - .to_str() - .unwrap() - .to_string() + .absolutize() + .unwrap() + .to_str() + .unwrap() + .to_string() } pub fn ensure_dir(path: &str) { let dir = match Path::new(path).is_dir() { true => path, - false => Path::new(path).parent().unwrap_or(Path::new("/")).to_str().unwrap_or("/"), + false => Path::new(path) + .parent() + .unwrap_or_else(|| Path::new("/")) + .to_str() + .unwrap_or("/"), }; create_dir_all(dir).unwrap(); diff --git a/src/pkey.rs b/src/pkey.rs new file mode 100644 index 0000000..41bb3e9 --- /dev/null +++ b/src/pkey.rs @@ -0,0 +1,44 @@ +use crate::path; +use crate::KeyType; +use openssl::ec::{EcGroup, EcKey}; +use openssl::nid::Nid; +use openssl::pkey::{PKey, Private}; +use openssl::rsa::Rsa; +use openssl::symm::Cipher; +use std::fs::{read, write}; + +pub fn generate_pkey(key_type: KeyType) -> PKey<Private> { + match key_type { + KeyType::Ecdsa => PKey::from_ec_key( + EcKey::generate(&EcGroup::from_curve_name(Nid::SECP384R1).unwrap()).unwrap(), + ) + .unwrap(), + KeyType::Rsa(bits) => PKey::from_rsa(Rsa::generate(bits).unwrap()).unwrap(), + } +} + +pub fn save_pkey(path: &str, key: &PKey<Private>, password: Option<String>) { + println!("{}", path); + path::ensure_dir(path); + + let pem_encoded = match password { + Some(pass) => { + // AES-256-GCM is recommended by this StackOverflow answer, but not supported in + // this function. AES-256-CBC is the alternative reccomendation, is supported + // https://stackoverflow.com/a/22958889 + key.private_key_to_pem_pkcs8_passphrase(Cipher::aes_256_cbc(), pass.as_bytes()) + .unwrap() + } + None => key.private_key_to_pem_pkcs8().unwrap(), + }; + write(path, pem_encoded).unwrap(); +} + +pub fn read_pkey(path: &str, password: Option<String>) -> PKey<Private> { + match password { + Some(pass) => { + PKey::private_key_from_pem_passphrase(&read(path).unwrap(), pass.as_bytes()).unwrap() + } + None => PKey::private_key_from_pem(&read(path).unwrap()).unwrap(), + } +} diff --git a/src/root.rs b/src/root.rs new file mode 100644 index 0000000..e5be068 --- /dev/null +++ b/src/root.rs @@ -0,0 +1,110 @@ +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::{Id, PKey, Private}; +use openssl::x509::extension::*; +use openssl::x509::*; + +use crate::path; +use std::fs::write; + +#[allow(clippy::too_many_arguments)] +pub fn generate_root_cert( + lifetime_days: u32, + common_name: &Option<String>, + country: &Option<String>, + state: &Option<String>, + locality: &Option<String>, + organization: &Option<String>, + organizational_unit: &Option<String>, + pkey: &PKey<Private>, +) -> X509 { + let mut x509_builder = X509::builder().unwrap(); + x509_builder.set_version(2).unwrap(); + + x509_builder + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + x509_builder + .set_not_after(&Asn1Time::days_from_now(lifetime_days).unwrap()) + .unwrap(); + + let mut serial = BigNum::new().unwrap(); + serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap(); + x509_builder + .set_serial_number(&serial.to_asn1_integer().unwrap()) + .unwrap(); + + let mut x509_name_builder = X509Name::builder().unwrap(); + if let Some(cn) = common_name { + x509_name_builder + .append_entry_by_nid(Nid::COMMONNAME, cn) + .unwrap(); + } + if let Some(c) = country { + x509_name_builder + .append_entry_by_nid(Nid::COUNTRYNAME, c) + .unwrap(); + } + if let Some(s) = state { + x509_name_builder + .append_entry_by_nid(Nid::STATEORPROVINCENAME, s) + .unwrap(); + } + if let Some(l) = locality { + x509_name_builder + .append_entry_by_nid(Nid::LOCALITYNAME, l) + .unwrap(); + } + if let Some(o) = organization { + x509_name_builder + .append_entry_by_nid(Nid::ORGANIZATIONNAME, o) + .unwrap(); + } + if let Some(ou) = organizational_unit { + x509_name_builder + .append_entry_by_nid(Nid::ORGANIZATIONALUNITNAME, ou) + .unwrap(); + } + let x509_name = x509_name_builder.build(); + + x509_builder.set_issuer_name(&x509_name).unwrap(); + x509_builder.set_subject_name(&x509_name).unwrap(); + + x509_builder.set_pubkey(pkey).unwrap(); + + let basic_constraints = BasicConstraints::new().critical().ca().build().unwrap(); + x509_builder.append_extension(basic_constraints).unwrap(); + + let key_usage = KeyUsage::new() + .critical() + .key_cert_sign() + .crl_sign() + .build() + .unwrap(); + x509_builder.append_extension(key_usage).unwrap(); + + let subject_key_identifier = SubjectKeyIdentifier::new() + .build(&x509_builder.x509v3_context(None, None)) + .unwrap(); + x509_builder + .append_extension(subject_key_identifier) + .unwrap(); + + let digest_algorithm = match pkey.id() { + Id::RSA => MessageDigest::sha256(), + Id::EC => MessageDigest::sha384(), + _ => MessageDigest::sha256(), + }; + + x509_builder.sign(pkey, digest_algorithm).unwrap(); + + x509_builder.build() +} + +pub fn save_root_cert(path: &str, cert: &X509) { + println!("{}", path); + path::ensure_dir(path); + write(path, cert.to_pem().unwrap()).unwrap(); +} |