diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-03-28 20:28:35 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-03-28 20:33:08 -0400 |
commit | 0a6292937e57dcf42c10c28aa52fb1a6efa79324 (patch) | |
tree | 7c3d267bc09e8836223fec9eb1caa0a24d74b99a | |
parent | cf43dfb5cba67da6b1929add44ae1d79a230095c (diff) |
issue simple certificates
-rw-r--r-- | src/cert.rs | 98 | ||||
-rw-r--r-- | src/cli.rs | 48 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 134 | ||||
-rw-r--r-- | src/path.rs | 14 | ||||
-rw-r--r-- | src/req.rs | 77 | ||||
-rw-r--r-- | src/root.rs | 9 |
7 files changed, 333 insertions, 49 deletions
diff --git a/src/cert.rs b/src/cert.rs new file mode 100644 index 0000000..3492617 --- /dev/null +++ b/src/cert.rs @@ -0,0 +1,98 @@ +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; +use openssl::hash::MessageDigest; +use openssl::pkey::{Id, PKey, Private}; +use openssl::x509::extension::*; +use openssl::x509::*; + +use crate::path; +use std::fs::{read, write}; + +#[allow(clippy::too_many_arguments)] +pub fn generate_cert( + lifetime_days: u32, + signing_request: &X509Req, + ca_cert: &X509, + ca_key_pair: &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(); + + x509_builder + .set_issuer_name(ca_cert.subject_name()) + .unwrap(); + x509_builder + .set_subject_name(signing_request.subject_name()) + .unwrap(); + + x509_builder + .set_pubkey(&signing_request.public_key().unwrap()) + .unwrap(); + + let basic_constraints = BasicConstraints::new().critical().build().unwrap(); + x509_builder.append_extension(basic_constraints).unwrap(); + + let key_usage = KeyUsage::new() + .critical() + .digital_signature() + .key_encipherment() + .build() + .unwrap(); + x509_builder.append_extension(key_usage).unwrap(); + + let extended_key_usage = ExtendedKeyUsage::new() + .client_auth() + .server_auth() + .build() + .unwrap(); + x509_builder.append_extension(extended_key_usage).unwrap(); + + let subject_key_identifier = SubjectKeyIdentifier::new() + .build(&x509_builder.x509v3_context(Some(ca_cert), None)) + .unwrap(); + x509_builder + .append_extension(subject_key_identifier) + .unwrap(); + + let authority_key_identifier = AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&x509_builder.x509v3_context(Some(ca_cert), None)) + .unwrap(); + x509_builder + .append_extension(authority_key_identifier) + .unwrap(); + + let digest_algorithm = match signing_request.public_key().unwrap().id() { + Id::RSA => MessageDigest::sha256(), + Id::EC => MessageDigest::sha384(), + _ => MessageDigest::sha256(), + }; + + x509_builder.sign(ca_key_pair, digest_algorithm).unwrap(); + + x509_builder.build() +} + +pub fn save_cert(path: &str, cert: &X509) { + println!("{}", path); + path::ensure_dir(path); + write(path, cert.to_pem().unwrap()).unwrap(); +} + +pub fn read_cert(path: &str) -> X509 { + X509::from_pem(&read(path).unwrap()).unwrap() +} @@ -11,6 +11,7 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { Init(Init), + Issue(Issue), } #[derive(Args, Debug)] @@ -60,6 +61,53 @@ pub struct Init { #[clap(long, short = 'p', env = "CA_PASSWORD")] pub password: Option<String>, } +#[derive(Args, Debug)] +#[clap(about = "Issue a new certificate")] +pub struct Issue { + /// 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, 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, short = 'b', default_value_t = 2048)] + pub key_length: u32, + + /// Lifetime in days of the generated certificate + #[clap(long, short = 'd', default_value_t = 90)] + pub lifetime: u32, + + /// Certificate CommonName + #[clap(long, short = 'n')] + pub common_name: String, + + /// Certificate Country + #[clap(long, short = 'c')] + pub country: Option<String>, + + /// Certificate State or Province + #[clap(long, short = 's')] + pub state: Option<String>, + + /// Certificate Locality + #[clap(long, short = 'l')] + pub locality: Option<String>, + + /// Certificate Organization + #[clap(long, short = 'o')] + pub organization: Option<String>, + + /// Certificate Organizational Unit + #[clap(long, short = 'u')] + pub organizational_unit: Option<String>, + + /// Password for private key + #[clap(long, short = 'p', env = "CA_PASSWORD")] + pub password: Option<String>, +} fn validate_key_type(input: &str) -> Result<(), String> { let input = input.to_string().to_uppercase(); @@ -1,5 +1,7 @@ +pub mod cert; pub mod path; pub mod pkey; +pub mod req; pub mod root; #[derive(Clone, Copy)] diff --git a/src/main.rs b/src/main.rs index 68d3b0e..6b6bd75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,6 @@ use clap::Parser; -use hancock::path; -use hancock::pkey; -use hancock::root; -use hancock::KeyType; +use hancock::*; use std::path::Path; @@ -14,40 +11,101 @@ fn main() { let cli = dbg!(Cli::parse()); match cli.command { - Commands::Init(args) => { - let base_dir = path::base_dir(&args.base_dir); - - let key_type = match args.key_type.to_uppercase().as_str() { - "RSA" => KeyType::Rsa(args.key_length), - "ECDSA" => KeyType::Ecdsa, - _ => panic!("key_type not ECDSA or RSA after validation. This should never happen"), - }; - - let pkey_path = path::ca_pkey(&base_dir, key_type); - - let pkey = match Path::new(&pkey_path).exists() { - true => pkey::read_pkey(&pkey_path, args.password), - false => { - let pkey = pkey::generate_pkey(key_type); - pkey::save_pkey(&pkey_path, &pkey, args.password); - pkey - } - }; - - 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); - } + Commands::Init(args) => init(args), + Commands::Issue(args) => issue(args), + } +} + +fn init(args: Init) { + let base_dir = path::base_dir(&args.base_dir); + + let key_type = match args.key_type.to_uppercase().as_str() { + "RSA" => KeyType::Rsa(args.key_length), + "ECDSA" => KeyType::Ecdsa, + _ => panic!("key_type not ECDSA or RSA after validation. This should never happen"), + }; + + let pkey_path = path::ca_pkey(&base_dir, key_type); + + let pkey = match Path::new(&pkey_path).exists() { + true => pkey::read_pkey(&pkey_path, args.password), + false => { + let pkey = pkey::generate_pkey(key_type); + pkey::save_pkey(&pkey_path, &pkey, args.password); + pkey } + }; + + 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, + ); + cert::save_cert(&cert_path, &cert); } } + +fn issue(args: Issue) { + let base_dir = path::base_dir(&args.base_dir); + + let key_type = match args.key_type.to_uppercase().as_str() { + "RSA" => KeyType::Rsa(args.key_length), + "ECDSA" => KeyType::Ecdsa, + _ => panic!("key_type not ECDSA or RSA after validation. This should never happen"), + }; + + let ca_pkey_path = path::ca_pkey(&base_dir, key_type); + + let ca_pkey = match Path::new(&ca_pkey_path).exists() { + true => pkey::read_pkey(&ca_pkey_path, args.password), + false => { + let pkey = pkey::generate_pkey(key_type); + pkey::save_pkey(&ca_pkey_path, &pkey, args.password); + pkey + } + }; + + let ca_cert_path = path::ca_crt(&base_dir, key_type); + let ca_cert = cert::read_cert(&ca_cert_path); + + let pkey_path = path::cert_pkey(&base_dir, &args.common_name, key_type); + let pkey = match Path::new(&pkey_path).exists() { + true => pkey::read_pkey(&pkey_path, None), + false => { + let pkey = pkey::generate_pkey(key_type); + pkey::save_pkey(&pkey_path, &pkey, None); + pkey + } + }; + + let x509_req_path = path::cert_csr(&base_dir, &args.common_name, key_type); + let x509_req = match Path::new(&x509_req_path).exists() { + true => req::read_req(&x509_req_path), + false => { + let req = req::generate_req( + &Some(args.common_name.clone()), + &args.country, + &args.state, + &args.locality, + &args.organization, + &args.organizational_unit, + &pkey, + ); + req::save_req(&x509_req_path, &req); + req + } + }; + + let cert = cert::generate_cert(args.lifetime, &x509_req, &ca_cert, &ca_pkey); + cert::save_cert( + &path::cert_crt(&base_dir, &args.common_name, key_type), + &cert, + ); +} diff --git a/src/path.rs b/src/path.rs index f13c591..d50f0f6 100644 --- a/src/path.rs +++ b/src/path.rs @@ -5,10 +5,20 @@ 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()) + format!("{base_dir}/authority.{}.pem", key_type.to_string()) } pub fn ca_crt(base_dir: &str, key_type: KeyType) -> String { - format!("{}/authority.{}.crt", base_dir, key_type.to_string()) + format!("{base_dir}/authority.{}.crt", key_type.to_string()) +} + +pub fn cert_pkey(base_dir: &str, name: &str, key_type: KeyType) -> String { + format!("{base_dir}/{name}/{name}.{}.pem", key_type.to_string()) +} +pub fn cert_csr(base_dir: &str, name: &str, key_type: KeyType) -> String { + format!("{base_dir}/{name}/{name}.{}.csr", key_type.to_string()) +} +pub fn cert_crt(base_dir: &str, name: &str, key_type: KeyType) -> String { + format!("{base_dir}/{name}/{name}.{}.crt", key_type.to_string()) } pub fn base_dir(raw_base: &str) -> String { diff --git a/src/req.rs b/src/req.rs new file mode 100644 index 0000000..d903b37 --- /dev/null +++ b/src/req.rs @@ -0,0 +1,77 @@ +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::{Id, PKey, Private}; +use openssl::x509::{X509Name, X509Req}; + +use std::fs::{read, write}; + +use crate::path; + +pub fn generate_req( + common_name: &Option<String>, + country: &Option<String>, + state: &Option<String>, + locality: &Option<String>, + organization: &Option<String>, + organizational_unit: &Option<String>, + pkey: &PKey<Private>, +) -> X509Req { + let mut x509req_builder = X509Req::builder().unwrap(); + + x509req_builder.set_pubkey(pkey).unwrap(); + x509req_builder.set_version(0).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(); + x509req_builder.set_subject_name(&x509_name).unwrap(); + + let digest_algorithm = match pkey.id() { + Id::RSA => MessageDigest::sha256(), + Id::EC => MessageDigest::sha384(), + _ => MessageDigest::sha256(), + }; + + x509req_builder.sign(pkey, digest_algorithm).unwrap(); + + x509req_builder.build() +} + +pub fn save_req(path: &str, req: &X509Req) { + println!("{}", path); + path::ensure_dir(path); + write(path, req.to_pem().unwrap()).unwrap(); +} + +pub fn read_req(path: &str) -> X509Req { + X509Req::from_pem(&read(path).unwrap()).unwrap() +} diff --git a/src/root.rs b/src/root.rs index e5be068..9b8393c 100644 --- a/src/root.rs +++ b/src/root.rs @@ -6,9 +6,6 @@ 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, @@ -102,9 +99,3 @@ pub fn generate_root_cert( 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(); -} |