summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Guyer <galen@galenguyer.com>2022-03-28 17:36:41 -0400
committerGalen Guyer <galen@galenguyer.com>2022-03-28 17:39:19 -0400
commitcf43dfb5cba67da6b1929add44ae1d79a230095c (patch)
treed32cad8343ddc34f556a0a84dc0331de0f4730bf
parentf86d704b3c638867e78718add25a762c44a06962 (diff)
save root key and certificate with password
-rw-r--r--.gitignore6
-rw-r--r--Cargo.lock14
-rw-r--r--Cargo.toml2
-rw-r--r--src/cli.rs31
-rw-r--r--src/lib.rs4
-rw-r--r--src/main.rs99
-rw-r--r--src/path.rs21
-rw-r--r--src/pkey.rs44
-rw-r--r--src/root.rs110
9 files changed, 230 insertions, 101 deletions
diff --git a/.gitignore b/.gitignore
index fedaa2b..98b52db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,8 @@
+# builds
/target
+# secrets
.env
+# certs
+*.pem
+*.crt
+*.csr
diff --git a/Cargo.lock b/Cargo.lock
index 925d5c0..9a637ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 2c0e2b9..ca6e16c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/src/cli.rs b/src/cli.rs
index 3f0f9a0..023d169 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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> {
diff --git a/src/lib.rs b/src/lib.rs
index 2c421ea..f0afd70 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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();
+}