Skip to content

Commit

Permalink
move former rcgen/main.rs to rcgen/examples/simple.rs
Browse files Browse the repository at this point in the history
Add basic functionality for rustls-cert-gen

This commit adds basic functionality for rustls-cerg-gen crate. A
small wrapper library has been added in order to organize code into
small modules and to provide a simple API that can easily be updated
as new functionality is added in the future. There are some rough
edges, some missing documentation for example. But I hope to get the
broader structure approved before refining the documentation. Serval
parameters have been added with the idea of supporting a broad range
of use-cases and test expectations around design and
maintainability. The easiest way to view currently supported options
is with `cargo run -- --help`.

Closes #175
  • Loading branch information
tbro committed Nov 9, 2023
1 parent 0318d2f commit 871d554
Show file tree
Hide file tree
Showing 14 changed files with 1,015 additions and 51 deletions.
317 changes: 309 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ members = ["rcgen", "rustls-cert-gen"]
resolver = "2"

[workspace.dependencies]
pem = { version = "3.0.2" }
pem = "3.0.2"
rand = "0.8"
ring = "0.17"
rsa = "0.9"
x509-parser = "0.15.1"

[workspace.package]
license = "MIT OR Apache-2.0"
Expand Down
10 changes: 5 additions & 5 deletions rcgen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 = { workspace = true }
38 changes: 38 additions & 0 deletions rcgen/examples/simple.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#![allow(clippy::complexity, clippy::style, clippy::pedantic)]

use rcgen::{date_time_ymd, Certificate, CertificateParams, DistinguishedName, DnType, SanType};
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
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())?;
Ok(())
}
9 changes: 8 additions & 1 deletion rustls-cert-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ 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 }

[dev-dependencies]
assert_fs = "1.0.13"
x509-parser = { workspace = true, features = ["verify"] }
30 changes: 30 additions & 0 deletions rustls-cert-gen/README.md
Original file line number Diff line number Diff line change
@@ -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`
75 changes: 75 additions & 0 deletions rustls-cert-gen/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Command Line argument parsing
#![allow(missing_docs)]

use std::{net::IpAddr, path::PathBuf};

use bpaf::Bpaf;
use rcgen::SanType;

#[derive(Clone, Debug, Bpaf)]

Check warning on line 9 in rustls-cert-gen/src/args.rs

View check run for this annotation

Codecov / codecov/patch

rustls-cert-gen/src/args.rs#L9

Added line #L9 was not covered by tests
#[bpaf(options)]
/// rustls-cert-gen Tls Certificate Generator
pub struct Options {
/// Output Directory for generated files
#[bpaf(short, long, argument("output/path/"))]
pub output: PathBuf,
/// Signature algorithm
#[bpaf(short, long, fallback("ecdsa_p256".into()), display_fallback)]
pub sig_algo: String,
#[bpaf(external)]
/// 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::<String>("san"), map(parse_sans))]
pub san: Vec<SanType>,
/// 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<String>) -> Vec<SanType> {
hosts.into_iter().map(parse_san).collect()
}

Check warning on line 50 in rustls-cert-gen/src/args.rs

View check run for this annotation

Codecov / codecov/patch

rustls-cert-gen/src/args.rs#L48-L50

Added lines #L48 - L50 were not covered by tests

fn parse_san(host: String) -> SanType {
if let Ok(ip) = host.parse::<IpAddr>() {
SanType::IpAddress(ip)
} else {
SanType::DnsName(host)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_san() {
let hosts = vec!["my.host.com", "localhost", "185.199.108.153"];
let sans: Vec<SanType> = hosts.into_iter().map(Into::into).map(parse_san).collect();
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]
);
}
}
75 changes: 75 additions & 0 deletions rustls-cert-gen/src/cert/ca.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use rcgen::{
BasicConstraints, Certificate, CertificateParams, DnType, DnValue::PrintableString, IsCa,
KeyUsagePurpose,
};

use super::PemCertifiedKey;

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 }
}
/// Return `&self.params`.
pub fn params(&self) -> &CertificateParams {
&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
}

Check warning on line 35 in rustls-cert-gen/src/cert/ca.rs

View check run for this annotation

Codecov / codecov/patch

rustls-cert-gen/src/cert/ca.rs#L24-L35

Added lines #L24 - L35 were not covered by tests
pub fn build(self) -> Result<Ca, rcgen::Error> {
let cert = Certificate::from_params(self.params)?;
let cert = Ca { cert };
Ok(cert)
}
}

pub struct Ca {
cert: Certificate,
}

impl Ca {
/// Self-sign and serialize
pub fn serialize_pem(&self) -> Result<PemCertifiedKey, rcgen::Error> {
let cert_pem = self.cert.serialize_pem()?;
let key_pem = self.cert.serialize_private_key_pem();
Ok(PemCertifiedKey {
cert_pem,
private_key_pem: key_pem,
})
}
pub fn cert(&self) -> &Certificate {
&self.cert
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn init_ca() {
let params = CertificateParams::default();
let cert = CaParams::new(params);
assert_eq!(
cert.params().is_ca,
IsCa::Ca(BasicConstraints::Unconstrained)
)
}
}
Loading

0 comments on commit 871d554

Please sign in to comment.