summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Guyer <galen@galenguyer.com>2022-03-28 20:28:35 -0400
committerGalen Guyer <galen@galenguyer.com>2022-03-28 20:33:08 -0400
commit0a6292937e57dcf42c10c28aa52fb1a6efa79324 (patch)
tree7c3d267bc09e8836223fec9eb1caa0a24d74b99a
parentcf43dfb5cba67da6b1929add44ae1d79a230095c (diff)
issue simple certificates
-rw-r--r--src/cert.rs98
-rw-r--r--src/cli.rs48
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs134
-rw-r--r--src/path.rs14
-rw-r--r--src/req.rs77
-rw-r--r--src/root.rs9
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()
+}
diff --git a/src/cli.rs b/src/cli.rs
index 023d169..573b2f5 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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();
diff --git a/src/lib.rs b/src/lib.rs
index f0afd70..745c200 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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();
-}