From 42fc2a51922fbe10cb2e830426d9e6488a72cf42 Mon Sep 17 00:00:00 2001 From: tbro Date: Fri, 27 Oct 2023 14:21:44 -0500 Subject: [PATCH] A functional `rustls-cert-gen` with basic parameters. This is basically #185 minus #188 and #189. The structure also differs as sub modules have been inlined in `main.rs` and `cert.rs`. `anyhow` has also been added as a dependency to replace the `Result` alias. Closes #175 includes review fixes such as: * remove top-level rsa dependency * inline parse_san * Check for presence of EKU before pushing. * Replace `struct Signature` struct w/ `enum KeypairAlgorithm` * update some doc strings * make EndEntity and Ca public so they appear in the docs --- Cargo.lock | 324 ++++++++++++++++++++++++++- Cargo.toml | 5 +- rcgen/Cargo.toml | 10 +- rustls-cert-gen/Cargo.toml | 10 +- rustls-cert-gen/README.md | 30 +++ rustls-cert-gen/src/cert.rs | 423 ++++++++++++++++++++++++++++++++++++ rustls-cert-gen/src/lib.rs | 8 + rustls-cert-gen/src/main.rs | 151 ++++++++++--- 8 files changed, 910 insertions(+), 51 deletions(-) create mode 100644 rustls-cert-gen/README.md create mode 100644 rustls-cert-gen/src/cert.rs create mode 100644 rustls-cert-gen/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c25ec2ad..8f3349f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "asn1-rs" version = "0.5.2" @@ -41,6 +62,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "assert_fs" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" +dependencies = [ + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -59,6 +95,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.0" @@ -89,6 +131,36 @@ dependencies = [ "botan-src", ] +[[package]] +name = "bpaf" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc3b1bd654a8d16eea03586c3eee8ffd25c7f242b9eae9730cc442834fe56d9" +dependencies = [ + "bpaf_derive", +] + +[[package]] +name = "bpaf_derive" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cbaba260bbcbb69b0d54e9f0d83038a4568f3a5d1c95591fed5e8fee964539" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "bstr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -169,6 +241,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -190,6 +268,40 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -226,6 +338,56 @@ dependencies = [ "wasi", ] +[[package]] +name = "globset" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -262,6 +424,12 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + [[package]] name = "log" version = "0.4.20" @@ -370,7 +538,7 @@ version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags", + "bitflags 2.4.0", "cfg-if", "foreign-types", "libc", @@ -454,6 +622,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +dependencies = [ + "anstyle", + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.67" @@ -510,7 +706,7 @@ dependencies = [ "openssl", "pem", "rand", - "ring 0.17.0", + "ring 0.17.5", "rsa", "rustls-webpki", "time", @@ -519,6 +715,44 @@ dependencies = [ "zeroize", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "ring" version = "0.16.20" @@ -536,9 +770,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.0" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9d44f9bf6b635117787f72416783eb7e4227aaf255e5ce739563d817176a7e" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" dependencies = [ "cc", "getrandom", @@ -550,16 +784,14 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" dependencies = [ - "byteorder", "const-oid", "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", @@ -579,12 +811,31 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustls-cert-gen" version = "0.1.0" dependencies = [ + "anyhow", + "assert_fs", + "bpaf", "pem", + "rand", "rcgen", + "ring 0.17.5", + "x509-parser", ] [[package]] @@ -597,6 +848,15 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.188" @@ -695,6 +955,25 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.49" @@ -715,6 +994,16 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.29" @@ -785,6 +1074,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -871,6 +1170,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 7454fbec..43641bee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,10 @@ members = ["rcgen", "rustls-cert-gen"] resolver = "2" [workspace.dependencies] -pem = { version = "3.0.2" } +pem = "3.0.2" +rand = "0.8" +ring = "0.17" +x509-parser = "0.15.1" [workspace.package] license = "MIT OR Apache-2.0" diff --git a/rcgen/Cargo.toml b/rcgen/Cargo.toml index eb4e6538..581fb4f2 100644 --- a/rcgen/Cargo.toml +++ b/rcgen/Cargo.toml @@ -23,10 +23,10 @@ required-features = ["pem", "x509-parser"] [dependencies] yasna = { version = "0.5.2", features = ["time", "std"] } -ring = "0.17" +ring = { workspace = true } pem = { workspace = true, optional = true } time = { version = "0.3.6", default-features = false } -x509-parser = { version = "0.15", features = ["verify"], optional = true } +x509-parser = { workspace = true, features = ["verify"], optional = true } zeroize = { version = "1.2", optional = true } [features] @@ -37,8 +37,8 @@ features = ["x509-parser"] [dev-dependencies] openssl = "0.10" -x509-parser = { version = "0.15", features = ["verify"] } +x509-parser = { workspace = true, features = ["verify"] } rustls-webpki = { version = "0.101.0", features = ["std"] } botan = { version = "0.10", features = ["vendored"] } -rand = "0.8" -rsa = "0.9" +rand = { workspace = true } +rsa = { version = "0.9" } diff --git a/rustls-cert-gen/Cargo.toml b/rustls-cert-gen/Cargo.toml index ed0267e4..b018070a 100644 --- a/rustls-cert-gen/Cargo.toml +++ b/rustls-cert-gen/Cargo.toml @@ -7,5 +7,13 @@ edition.workspace = true keywords.workspace = true [dependencies] -rcgen = { path = "../rcgen" } +bpaf = { version = "0.9.5", features = ["derive"] } pem = { workspace = true } +rcgen = { path = "../rcgen" } +ring = { workspace = true } +rand = { workspace = true } +anyhow = "1.0.75" + +[dev-dependencies] +assert_fs = "1.0.13" +x509-parser = { workspace = true, features = ["verify"] } diff --git a/rustls-cert-gen/README.md b/rustls-cert-gen/README.md new file mode 100644 index 00000000..2b08073e --- /dev/null +++ b/rustls-cert-gen/README.md @@ -0,0 +1,30 @@ +# rustls-cert-gen + +`rustls-cert-gen` is a tool to generate TLS certificates. In its +current state it will generate a Root CA and an end-entity +certificate, along with private keys. The end-entity certificate will +be signed by the Root CA. + +## Usage +Having compiled the binary you can simply pass a path to output +generated files. + + cargo run -- -o output/dir + +In the output directory you will find these files: + + * `cert.pem` (end-entity's X.509 certificate, signed by `root-ca`'s key) + * `cert.key.pem` (end-entity's private key) + * `root-ca.pem` (ca's self-signed X.509 certificate) + +For a complete list of supported options: + + rustls-cert-gen --help + +## FAQ + +#### What signature schemes are available? + + * `pkcs_ecdsa_p256_sha256` + * `pkcs_ecdsa_p384_sha384` + * `pkcs_ed25519` diff --git a/rustls-cert-gen/src/cert.rs b/rustls-cert-gen/src/cert.rs new file mode 100644 index 00000000..55ed867f --- /dev/null +++ b/rustls-cert-gen/src/cert.rs @@ -0,0 +1,423 @@ +use bpaf::Bpaf; +use rcgen::{ + BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, + DnValue::PrintableString, ExtendedKeyUsagePurpose, IsCa, KeyUsagePurpose, SanType, +}; +use std::{fmt, fs::File, io, path::Path}; + +#[derive(Debug, Clone)] +/// Pem serialized Certificate and Pem serialized corresponding private key +pub struct PemCertifiedKey { + pub cert_pem: String, + pub private_key_pem: String, +} + +impl PemCertifiedKey { + pub fn write(&self, dir: &Path, name: &str) -> Result<(), io::Error> { + use std::io::Write; + std::fs::create_dir_all(dir)?; + + let key_path = dir.join(format!("{name}.key.pem")); + let mut key_out = File::create(key_path)?; + write!(key_out, "{}", &self.private_key_pem)?; + + let cert_path = dir.join(format!("{name}.pem")); + let mut cert_out = File::create(cert_path)?; + write!(cert_out, "{}", &self.cert_pem)?; + + Ok(()) + } +} + +/// Builder to configure TLS [CertificateParams] to be finalized +/// into either a [Ca] or an [EndEntity]. +#[derive(Default)] +pub struct CertificateBuilder { + params: CertificateParams, +} + +impl CertificateBuilder { + /// Initialize `CertificateParams` with defaults + /// # Example + /// ``` + /// # use rustls_cert_gen::CertificateBuilder; + /// let cert = CertificateBuilder::new(); + /// ``` + pub fn new() -> Self { + let mut params = CertificateParams::default(); + // override default Common Name + params.distinguished_name = DistinguishedName::new(); + Self { params } + } + /// Set signature algorithm (instead of default). + pub fn signature_algorithm(mut self, alg: &KeypairAlgorithm) -> anyhow::Result { + let keypair = alg.to_keypair()?; + self.params.alg = keypair.algorithm(); + self.params.key_pair = Some(keypair); + Ok(self) + } + /// Set options for Ca Certificates + /// # Example + /// ``` + /// # use rustls_cert_gen::CertificateBuilder; + /// let cert = CertificateBuilder::new().certificate_authority(); + /// ``` + pub fn certificate_authority(self) -> CaParams { + CaParams::new(self.params) + } + /// Set options for `EndEntity` Certificates + pub fn end_entity(self) -> EndEntityParams { + EndEntityParams::new(self.params) + } +} + +pub struct CaParams { + params: CertificateParams, +} + +impl CaParams { + pub fn new(mut params: CertificateParams) -> Self { + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.key_usages.push(KeyUsagePurpose::DigitalSignature); + params.key_usages.push(KeyUsagePurpose::KeyCertSign); + params.key_usages.push(KeyUsagePurpose::CrlSign); + Self { params } + } + pub fn country_name(mut self, country: &str) -> Self { + self.params + .distinguished_name + .push(DnType::CountryName, PrintableString(country.into())); + self + } + pub fn organization_name(mut self, name: &str) -> Self { + self.params + .distinguished_name + .push(DnType::OrganizationName, name); + self + } + pub fn build(self) -> Result { + Ok(Ca { + cert: Certificate::from_params(self.params)?, + }) + } +} + +/// End-entity [Certificate] +pub struct Ca { + cert: Certificate, +} + +impl Ca { + /// Self-sign and serialize + pub fn serialize_pem(&self) -> Result { + Ok(PemCertifiedKey { + cert_pem: self.cert.serialize_pem()?, + private_key_pem: self.cert.serialize_private_key_pem(), + }) + } + /// Return `&Certificate` + pub fn cert(&self) -> &Certificate { + &self.cert + } +} + +/// End-entity [Certificate] +pub struct EndEntity { + cert: Certificate, +} + +impl EndEntity { + /// Sign with `self.signer` and serialize. + pub fn serialize_pem(&self, signer: &Certificate) -> Result { + Ok(PemCertifiedKey { + cert_pem: self.cert.serialize_pem_with_signer(signer)?, + private_key_pem: self.cert.serialize_private_key_pem(), + }) + } +} + +/// [CertificateParams] from which an [EndEntity] [Certificate] can be built +pub struct EndEntityParams { + params: CertificateParams, +} + +impl EndEntityParams { + /// Initialize `EndEntityParams` + pub fn new(mut params: CertificateParams) -> Self { + params.is_ca = IsCa::NoCa; + params.use_authority_key_identifier_extension = true; + params.key_usages.push(KeyUsagePurpose::DigitalSignature); + Self { params } + } + /// Add CommonName to `distinquished_names`. Multiple calls will + /// replace previous value. + pub fn common_name(mut self, name: &str) -> Self { + self.params + .distinguished_name + .push(DnType::CommonName, name); + self + } + /// `SanTypes` that will be recorded as + /// `subject_alt_names`. Multiple calls will append to previous + /// values. + pub fn subject_alternative_names(mut self, sans: Vec) -> Self { + self.params.subject_alt_names.extend(sans); + self + } + /// Add ClientAuth to `extended_key_usages` if it is not already present. + pub fn client_auth(&mut self) -> &Self { + let usage = ExtendedKeyUsagePurpose::ClientAuth; + if !self.params.extended_key_usages.iter().any(|e| e == &usage) { + self.params.extended_key_usages.push(usage); + } + self + } + /// Add ServerAuth to `extended_key_usages` if it is not already present. + pub fn server_auth(&mut self) -> &Self { + let usage = ExtendedKeyUsagePurpose::ServerAuth; + if !self.params.extended_key_usages.iter().any(|e| e == &usage) { + self.params.extended_key_usages.push(usage); + } + self + } + pub fn build(self) -> Result { + Ok(EndEntity { + cert: Certificate::from_params(self.params)?, + }) + } +} + +#[derive(Clone, Debug, Bpaf)] +/// Supported Keypair Algorithms +pub enum KeypairAlgorithm { + Ed25519, + EcdsaP256, + EcdsaP384, +} + +impl fmt::Display for KeypairAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeypairAlgorithm::Ed25519 => write!(f, "ed25519"), + KeypairAlgorithm::EcdsaP256 => write!(f, "ecdsa-p256"), + KeypairAlgorithm::EcdsaP384 => write!(f, "ecdsa-p384"), + } + } +} + +impl KeypairAlgorithm { + /// Return an `rcgen::KeyPair` for the given varient + fn to_keypair(&self) -> Result { + match self { + KeypairAlgorithm::Ed25519 => { + use ring::signature::Ed25519KeyPair; + + let rng = ring::rand::SystemRandom::new(); + let alg = &rcgen::PKCS_ED25519; + let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rng)?; + + rcgen::KeyPair::from_der_and_sign_algo(pkcs8_bytes.as_ref(), alg) + }, + KeypairAlgorithm::EcdsaP256 => { + use ring::signature::EcdsaKeyPair; + use ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING; + + let rng = ring::rand::SystemRandom::new(); + let alg = &rcgen::PKCS_ECDSA_P256_SHA256; + let pkcs8_bytes = + EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)?; + rcgen::KeyPair::from_der_and_sign_algo(pkcs8_bytes.as_ref(), alg) + }, + KeypairAlgorithm::EcdsaP384 => { + use ring::signature::EcdsaKeyPair; + use ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING; + + let rng = ring::rand::SystemRandom::new(); + let alg = &rcgen::PKCS_ECDSA_P384_SHA384; + let pkcs8_bytes = + EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_ASN1_SIGNING, &rng)?; + + rcgen::KeyPair::from_der_and_sign_algo(pkcs8_bytes.as_ref(), alg) + }, + } + } +} + +#[cfg(test)] +mod tests { + use x509_parser::prelude::{FromDer, X509Certificate}; + + use super::*; + + #[test] + fn test_write_files() -> anyhow::Result<()> { + use assert_fs::prelude::*; + let temp = assert_fs::TempDir::new()?; + let dir = temp.path(); + let entity_cert = temp.child("cert.pem"); + let entity_key = temp.child("cert.key.pem"); + + let pck = PemCertifiedKey { + cert_pem: "x".into(), + private_key_pem: "y".into(), + }; + + pck.write(dir, "cert")?; + + // assert contents of created files + entity_cert.assert("x"); + entity_key.assert("y"); + + Ok(()) + } + #[test] + fn init_ca() { + let cert = CertificateBuilder::new().certificate_authority(); + assert_eq!(cert.params.is_ca, IsCa::Ca(BasicConstraints::Unconstrained)) + } + #[test] + fn with_sig_algo_default() -> anyhow::Result<()> { + let end_entity = CertificateBuilder::new().end_entity(); + + assert_eq!(end_entity.params.alg, &rcgen::PKCS_ECDSA_P256_SHA256); + Ok(()) + } + #[test] + fn serialize_end_entity_default_sig() -> anyhow::Result<()> { + let ca = CertificateBuilder::new().certificate_authority().build()?; + let end_entity = CertificateBuilder::new() + .end_entity() + .build()? + .serialize_pem(ca.cert())?; + + let der = pem::parse(end_entity.cert_pem)?; + let (_, cert) = X509Certificate::from_der(der.contents())?; + + let issuer_der = pem::parse(ca.serialize_pem()?.cert_pem)?; + let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; + + assert!(!cert.is_ca()); + check_signature(&cert, &issuer); + + Ok(()) + } + #[test] + fn serialize_end_entity_ecdsa_p384_sha384_sig() -> anyhow::Result<()> { + let ca = CertificateBuilder::new().certificate_authority().build()?; + let end_entity = CertificateBuilder::new() + .signature_algorithm(&KeypairAlgorithm::EcdsaP384)? + .end_entity() + .build()? + .serialize_pem(ca.cert())?; + + let der = pem::parse(end_entity.cert_pem)?; + let (_, cert) = X509Certificate::from_der(der.contents())?; + + let issuer_der = pem::parse(ca.serialize_pem()?.cert_pem)?; + let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; + + check_signature(&cert, &issuer); + Ok(()) + } + + #[test] + fn serialize_end_entity_ed25519_sig() -> anyhow::Result<()> { + let ca = CertificateBuilder::new().certificate_authority().build()?; + let end_entity = CertificateBuilder::new() + .signature_algorithm(&KeypairAlgorithm::Ed25519)? + .end_entity() + .build()? + .serialize_pem(ca.cert())?; + + let der = pem::parse(end_entity.cert_pem)?; + let (_, cert) = X509Certificate::from_der(der.contents())?; + + let issuer_der = pem::parse(ca.serialize_pem()?.cert_pem)?; + let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; + + check_signature(&cert, &issuer); + Ok(()) + } + pub fn check_signature(cert: &X509Certificate<'_>, issuer: &X509Certificate<'_>) { + let verified = cert.verify_signature(Some(issuer.public_key())).is_ok(); + assert!(verified); + } + + #[test] + fn init_end_endity() { + let params = CertificateParams::default(); + let cert = EndEntityParams::new(params); + assert_eq!(cert.params.is_ca, IsCa::NoCa) + } + #[test] + fn client_auth_end_entity() { + let _ca = CertificateBuilder::new() + .certificate_authority() + .build() + .unwrap(); + let params = CertificateParams::default(); + let mut cert = EndEntityParams::new(params); + assert_eq!(cert.params.is_ca, IsCa::NoCa); + assert_eq!( + cert.client_auth().params.extended_key_usages, + vec![ExtendedKeyUsagePurpose::ClientAuth] + ); + } + #[test] + fn server_auth_end_entity() { + let _ca = CertificateBuilder::new() + .certificate_authority() + .build() + .unwrap(); + let params = CertificateParams::default(); + let mut cert = EndEntityParams::new(params); + assert_eq!(cert.params.is_ca, IsCa::NoCa); + assert_eq!( + cert.server_auth().params.extended_key_usages, + vec![ExtendedKeyUsagePurpose::ServerAuth] + ); + } + #[test] + fn sans_end_entity() { + let _ca = CertificateBuilder::new() + .certificate_authority() + .build() + .unwrap(); + let name = "unexpected.oomyoo.xyz"; + let names = vec![SanType::DnsName(name.into())]; + let params = CertificateParams::default(); + let cert = EndEntityParams::new(params).subject_alternative_names(names); + assert_eq!( + cert.params.subject_alt_names, + vec![rcgen::SanType::DnsName(name.into())] + ); + } + #[test] + fn sans_end_entity_empty() { + let _ca = CertificateBuilder::new() + .certificate_authority() + .build() + .unwrap(); + let names = vec![]; + let params = CertificateParams::default(); + let cert = EndEntityParams::new(params).subject_alternative_names(names); + assert_eq!(cert.params.subject_alt_names, vec![]); + } + + #[test] + fn keypair_algorithm_to_keypair() -> anyhow::Result<()> { + let keypair = KeypairAlgorithm::Ed25519.to_keypair()?; + assert_eq!(format!("{:?}", keypair.algorithm()), "PKCS_ED25519"); + let keypair = KeypairAlgorithm::EcdsaP256.to_keypair()?; + assert_eq!( + format!("{:?}", keypair.algorithm()), + "PKCS_ECDSA_P256_SHA256" + ); + let keypair = KeypairAlgorithm::EcdsaP384.to_keypair()?; + assert_eq!( + format!("{:?}", keypair.algorithm()), + "PKCS_ECDSA_P384_SHA384" + ); + Ok(()) + } +} diff --git a/rustls-cert-gen/src/lib.rs b/rustls-cert-gen/src/lib.rs new file mode 100644 index 00000000..550d5b45 --- /dev/null +++ b/rustls-cert-gen/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(missing_docs)] +//! This library wraps [rcgen] to provide a simple API to generate TLS +//! certificate-chains. Its primary intent is to ease development of +//! applications that verify chain of trust. It can be used for +//! whatever purpose you may need a TLS certificate-chain. + +mod cert; +pub use cert::{Ca, CertificateBuilder, EndEntity}; diff --git a/rustls-cert-gen/src/main.rs b/rustls-cert-gen/src/main.rs index 9a6ad87f..9b9f3c44 100644 --- a/rustls-cert-gen/src/main.rs +++ b/rustls-cert-gen/src/main.rs @@ -1,38 +1,117 @@ -#![allow(clippy::complexity, clippy::style, clippy::pedantic)] - -use rcgen::{date_time_ymd, Certificate, CertificateParams, DistinguishedName, DnType, SanType}; -use std::fs; - -fn main() -> Result<(), Box> { - let mut params: CertificateParams = Default::default(); - params.not_before = date_time_ymd(1975, 01, 01); - params.not_after = date_time_ymd(4096, 01, 01); - params.distinguished_name = DistinguishedName::new(); - params - .distinguished_name - .push(DnType::OrganizationName, "Crab widgits SE"); - params - .distinguished_name - .push(DnType::CommonName, "Master Cert"); - params.subject_alt_names = vec![ - SanType::DnsName("crabs.crabs".to_string()), - SanType::DnsName("localhost".to_string()), - ]; - - let cert = Certificate::from_params(params)?; - - let pem_serialized = cert.serialize_pem()?; - let pem = pem::parse(&pem_serialized)?; - let der_serialized = pem.contents(); - println!("{pem_serialized}"); - println!("{}", cert.serialize_private_key_pem()); - std::fs::create_dir_all("certs/")?; - fs::write("certs/cert.pem", &pem_serialized.as_bytes())?; - fs::write("certs/cert.der", &der_serialized)?; - fs::write( - "certs/key.pem", - &cert.serialize_private_key_pem().as_bytes(), - )?; - fs::write("certs/key.der", &cert.serialize_private_key_der())?; +use bpaf::Bpaf; +use rcgen::SanType; +use std::{net::IpAddr, path::PathBuf}; + +mod cert; +use cert::{keypair_algorithm, CertificateBuilder, KeypairAlgorithm}; + +fn main() -> anyhow::Result<()> { + let opts = options().run(); + + let ca = CertificateBuilder::new() + .signature_algorithm(&opts.keypair_algorithm)? + .certificate_authority() + .country_name(&opts.country_name) + .organization_name(&opts.organization_name) + .build()?; + + let mut entity = CertificateBuilder::new() + .signature_algorithm(&opts.keypair_algorithm)? + .end_entity() + .common_name(&opts.common_name) + .subject_alternative_names(opts.san); + + if opts.client_auth { + entity.client_auth(); + }; + + if opts.server_auth { + entity.server_auth(); + }; + + entity + .build()? + .serialize_pem(ca.cert())? + .write(&opts.output, &opts.cert_file_name)?; + + ca.serialize_pem()? + .write(&opts.output, &opts.ca_file_name)?; + Ok(()) } + +#[derive(Clone, Debug, Bpaf)] +#[bpaf(options)] +/// rustls-cert-gen Tls Certificate Generator +struct Options { + /// Output Directory for generated files + #[bpaf(short, long, argument("output/path/"))] + pub output: PathBuf, + /// Keypair algorithm + #[bpaf( + external(keypair_algorithm), + fallback(KeypairAlgorithm::EcdsaP256), + display_fallback, + group_help("Keypair Algorithm:") + )] + pub keypair_algorithm: KeypairAlgorithm, + /// Extended Key Usage Purpose: ClientAuth + #[bpaf(long)] + pub client_auth: bool, + /// Extended Key Usage Purpose: ServerAuth + #[bpaf(long)] + pub server_auth: bool, + /// Basename for end-entity cert/key + #[bpaf(long, fallback("cert".into()), display_fallback)] + pub cert_file_name: String, + /// Basename for ca cert/key + #[bpaf(long, fallback("root-ca".into()), display_fallback)] + pub ca_file_name: String, + /// Subject Alt Name (apply multiple times for multiple names/Ips) + #[bpaf(many, long, argument::("san"), map(parse_sans))] + pub san: Vec, + /// Common Name (Currently only used for end-entity) + #[bpaf(long, fallback("Tls End-Entity Certificate".into()), display_fallback)] + pub common_name: String, + /// Country Name (Currently only used for ca) + #[bpaf(long, fallback("BR".into()), display_fallback)] + pub country_name: String, + /// Organization Name (Currently only used for ca) + #[bpaf(long, fallback("Crab widgits SE".into()), display_fallback)] + pub organization_name: String, +} + +/// Parse cli input into SanType. Try first `IpAddr`, if that fails +/// declare it to be a DnsName. +fn parse_sans(hosts: Vec) -> Vec { + hosts + .into_iter() + .map(|host| { + if let Ok(ip) = host.parse::() { + SanType::IpAddress(ip) + } else { + SanType::DnsName(host) + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_san() { + let hosts = vec!["my.host.com", "localhost", "185.199.108.153"] + .into_iter() + .map(Into::into) + .collect(); + let sans: Vec = parse_sans(hosts); + assert_eq!(SanType::DnsName("my.host.com".into()), sans[0]); + assert_eq!(SanType::DnsName("localhost".into()), sans[1]); + assert_eq!( + SanType::IpAddress("185.199.108.153".parse().unwrap()), + sans[2] + ); + } +}