diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84762fb8..ef5d7233 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,6 +96,8 @@ jobs: - name: Check semver uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + exclude: rustls-cert-gen build-windows: runs-on: windows-latest diff --git a/Cargo.lock b/Cargo.lock index 5223403d..210752ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,18 @@ 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" @@ -50,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" @@ -100,7 +127,7 @@ version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ - "bitflags", + "bitflags 2.4.1", "cexpr", "clang-sys", "lazy_static", @@ -117,6 +144,12 @@ dependencies = [ "which", ] +[[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.1" @@ -147,6 +180,36 @@ dependencies = [ "botan-src", ] +[[package]] +name = "bpaf" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19232d7d855392d993f6dabd8dea40a457a6d24ef679fe98f5edca811bb11e21" +dependencies = [ + "bpaf_derive", +] + +[[package]] +name = "bpaf_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efeab2975f8102de445dcf898856a638332403c50216144653a89aec22fd79e0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -209,6 +272,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "crossbeam-utils" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -259,6 +331,12 @@ dependencies = [ "powerfmt", ] +[[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" @@ -280,6 +358,12 @@ dependencies = [ "syn 2.0.41", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dunce" version = "1.0.4" @@ -302,6 +386,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "foreign-types" version = "0.3.2" @@ -350,6 +440,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[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 = "home" version = "0.5.5" @@ -359,6 +473,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -531,7 +672,7 @@ version = "0.10.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" dependencies = [ - "bitflags", + "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", @@ -633,6 +774,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" +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 = "prettyplease" version = "0.2.15" @@ -709,6 +878,15 @@ 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" @@ -808,7 +986,7 @@ version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -819,8 +997,14 @@ dependencies = [ name = "rustls-cert-gen" version = "0.1.0" dependencies = [ + "anyhow", + "assert_fs", + "bpaf", "pem", + "rand", "rcgen", + "ring 0.17.7", + "x509-parser", ] [[package]] @@ -833,6 +1017,15 @@ dependencies = [ "untrusted 0.9.0", ] +[[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.193" @@ -937,6 +1130,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 0.48.0", +] + +[[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.50" @@ -957,6 +1169,16 @@ dependencies = [ "syn 2.0.41", ] +[[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.30" @@ -1028,6 +1250,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" @@ -1126,6 +1358,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 955e545b..b529d9e5 100644 --- a/rcgen/Cargo.toml +++ b/rcgen/Cargo.toml @@ -24,10 +24,10 @@ required-features = ["pem", "x509-parser"] [dependencies] aws-lc-rs = { version = "1.0.0", optional = true } yasna = { version = "0.5.2", features = ["time", "std"] } -ring = { version = "0.17", optional = true } +ring = { workspace = true, optional = 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] @@ -35,6 +35,7 @@ default = ["pem", "ring"] aws_lc_rs = ["dep:aws-lc-rs"] ring = ["dep:ring"] + [package.metadata.docs.rs] features = ["x509-parser"] @@ -46,9 +47,9 @@ allowed_external_types = [ [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" +rand = { workspace = true } rsa = "0.9" -ring = "0.17" +ring = { workspace = true } diff --git a/rustls-cert-gen/Cargo.toml b/rustls-cert-gen/Cargo.toml index 103cf4ea..b330531c 100644 --- a/rustls-cert-gen/Cargo.toml +++ b/rustls-cert-gen/Cargo.toml @@ -8,4 +8,12 @@ keywords.workspace = true [dependencies] rcgen = { path = "../rcgen", default-features = false, features = ["pem"] } +bpaf = { version = "0.9.5", features = ["derive"] } pem = { workspace = true } +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..b28041cf --- /dev/null +++ b/rustls-cert-gen/src/cert.rs @@ -0,0 +1,434 @@ +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) -> CaBuilder { + CaBuilder::new(self.params) + } + /// Set options for `EndEntity` Certificates + pub fn end_entity(self) -> EndEntityBuilder { + EndEntityBuilder::new(self.params) + } +} + +/// [CertificateParams] from which an [Ca] [Certificate] can be built +pub struct CaBuilder { + params: CertificateParams, +} + +impl CaBuilder { + /// Initialize `CaBuilder` + 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 } + } + /// Add CountryName to `distinguished_name`. Multiple calls will + /// replace previous value. + pub fn country_name(mut self, country: &str) -> Self { + self.params + .distinguished_name + .push(DnType::CountryName, PrintableString(country.into())); + self + } + /// Add OrganizationName to `distinguished_name`. Multiple calls will + /// replace previous value. + pub fn organization_name(mut self, name: &str) -> Self { + self.params + .distinguished_name + .push(DnType::OrganizationName, name); + self + } + /// build `Ca` Certificate. + 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 `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 EndEntityBuilder { + params: CertificateParams, +} + +impl EndEntityBuilder { + /// Initialize `EndEntityBuilder` + 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 `distinguished_name`. 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 + } + /// build `EndEntity` Certificate. + 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).or(Err(rcgen::Error::RingUnspecified))?; + + 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) + .or(Err(rcgen::Error::RingUnspecified))?; + 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) + .or(Err(rcgen::Error::RingUnspecified))?; + + 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 = EndEntityBuilder::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 = EndEntityBuilder::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 = EndEntityBuilder::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 = EndEntityBuilder::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 = EndEntityBuilder::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..de7b13e1 --- /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 chains of trust. It can be used for +//! whatever purpose you may need a TLS certificate-chain. + +mod cert; +pub use cert::{Ca, CaBuilder, CertificateBuilder, EndEntity, EndEntityBuilder}; diff --git a/rustls-cert-gen/src/main.rs b/rustls-cert-gen/src/main.rs index 9a6ad87f..a6dfa25c 100644 --- a/rustls-cert-gen/src/main.rs +++ b/rustls-cert-gen/src/main.rs @@ -1,38 +1,126 @@ -#![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", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + ] + .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] + ); + assert_eq!( + SanType::IpAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334".parse().unwrap()), + sans[3] + ); + } +}