Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rustls-cert-gen support basic parameters #185

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
tbro marked this conversation as resolved.
Show resolved Hide resolved
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