Skip to content
Open
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
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[alias]
cert-tools = ["run", "-p", "cert-tools", "--"]
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 25 additions & 23 deletions Cargo.nix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion rust/cert-tools/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cert-tools"
description = "A CLI tool to merge two truststores in PEM or PKCS12 format in such as way that they are accepted by the JVM"
description = "Merge multiple truststores encoded as PEM or PKCS12 into a JVM compatible format"
version = "0.0.0-dev"
authors.workspace = true
license.workspace = true
Expand All @@ -18,3 +18,6 @@ openssl.workspace = true
snafu.workspace = true
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }

[build-dependencies]
built.workspace = true
3 changes: 3 additions & 0 deletions rust/cert-tools/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
built::write_built_file().unwrap();
}
28 changes: 20 additions & 8 deletions rust/cert-tools/src/cert_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,36 @@ use openssl::{
string::OpensslString,
x509::X509,
};
use snafu::ResultExt;
use snafu::{ResultExt, Snafu};

#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("failed to convert certificate serial number to BigNum"))]
ConvertSerialToBigNum { source: openssl::error::ErrorStack },

#[snafu(display("failed to convert certificate serial number to a hexadecimal string"))]
ConvertSerialToHexString { source: openssl::error::ErrorStack },

#[snafu(display("failed to retireve certificate digest as SHA256"))]
RetrieveDigest { source: openssl::error::ErrorStack },
}

pub trait CertExt {
fn serial_as_hex(&self) -> Result<OpensslString, snafu::Whatever>;
fn sha256_digest(&self) -> Result<DigestBytes, snafu::Whatever>;
fn serial_as_hex(&self) -> Result<OpensslString, Error>;
fn sha256_digest(&self) -> Result<DigestBytes, Error>;
}

impl CertExt for X509 {
fn serial_as_hex(&self) -> Result<OpensslString, snafu::Whatever> {
fn serial_as_hex(&self) -> Result<OpensslString, Error> {
self.serial_number()
.to_bn()
.whatever_context("failed to get certificate serial number as BigNumber")?
.context(ConvertSerialToBigNumSnafu)?
.to_hex_str()
.whatever_context("failed to convert certificate serial number to hex string")
.context(ConvertSerialToHexStringSnafu)
}

fn sha256_digest(&self) -> Result<DigestBytes, snafu::Whatever> {
fn sha256_digest(&self) -> Result<DigestBytes, Error> {
self.digest(MessageDigest::sha256())
.whatever_context("failed to get certificate digest")
.context(RetrieveDigestSnafu)
}
}
136 changes: 136 additions & 0 deletions rust/cert-tools/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use std::{fs, path::PathBuf, str::FromStr};

use clap::{Parser, Subcommand};
use openssl::x509::X509;
use snafu::{OptionExt, ResultExt, Snafu, ensure};
use stackable_telemetry::tracing::TelemetryOptions;

use crate::parsers::{pem, pkcs12};

#[derive(Parser, Debug)]
#[command(version, about)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,

#[command(flatten, next_help_heading = "Tracing options")]
pub telemetry: TelemetryOptions,
}

#[derive(Subcommand, Debug)]
pub enum Command {
/// Generate PKCS12 truststore files from PEM or PKCS12 files
GeneratePkcs12Truststore(GeneratePkcs12TruststoreArguments),
}

#[derive(Parser, Debug)]
pub struct GeneratePkcs12TruststoreArguments {
/// The path to output the resulting PKCS12 to
#[arg(long)]
pub out: PathBuf,

/// The password used to encrypt the outputted PKCS12 truststore. Defaults to an empty string.
#[arg(long, default_value = "")]
pub out_password: String,

/// List of PEM certificate(s)
#[arg(long = "pem")]
pub pems: Vec<PathBuf>,

/// List of PKCS12 truststore(s)
///
/// You can either use `truststore.p12` (which uses an empty password by default), or specify
/// the password using `truststore.p12:changeit`.
#[arg(long = "pkcs12", value_parser = Pkcs12Source::from_str)]
pub pkcs12s: Vec<Pkcs12Source>,
}

#[derive(Debug, Snafu)]
#[snafu(display("missing path"))]
pub struct Pkcs12SourceParseError;

#[derive(Clone, Debug)]
pub struct Pkcs12Source {
path: PathBuf,
password: String,
}

impl FromStr for Pkcs12Source {
type Err = Pkcs12SourceParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(2, ':');
let path = parts.next().context(Pkcs12SourceParseSnafu)?;
let password = parts.next().unwrap_or("").to_owned();

Ok(Self {
path: PathBuf::from(path),
password,
})
}
}

impl GeneratePkcs12TruststoreArguments {
pub fn certificate_sources(&self) -> Vec<CertInput> {
let pems = self.pems.iter().cloned().map(CertInput::Pem);
let pkcs12s = self.pkcs12s.iter().cloned().map(CertInput::Pkcs12);
pems.chain(pkcs12s).collect()
}
}

#[derive(Debug, Snafu)]
pub enum CertInputError {
#[snafu(display("failed to read from file at {path}", path = path.display()))]
ReadFile {
source: std::io::Error,
path: PathBuf,
},

#[snafu(display("failed to parse file contents as PEM"))]
ParseFileAsPem {
source: crate::parsers::pem::ParseError,
},

#[snafu(display("failed to parse file contents as PKCS#12"))]
ParseFileAsPkcs12 {
source: crate::parsers::pkcs12::WorkaroundError,
},

#[snafu(display("the PEM file at {path} contained no certificates", path = path.display()))]
NoCertificates { path: PathBuf },
}

#[derive(Debug)]
pub enum CertInput {
Pem(PathBuf),
Pkcs12(Pkcs12Source),
}

impl CertInput {
pub fn from_file(&self) -> Result<Vec<X509>, CertInputError> {
let read_file_fn = |path| fs::read(path).context(ReadFileSnafu { path });

match self {
CertInput::Pem(path) => {
let file_contents = read_file_fn(path)?;

let certs = pem::parse_contents(&file_contents).context(ParseFileAsPemSnafu)?;
ensure!(!certs.is_empty(), NoCertificatesSnafu { path });

Ok(certs)
}
CertInput::Pkcs12(Pkcs12Source { path, password }) => {
let file_contents = read_file_fn(path)?;
pkcs12::parse_file_workaround(&file_contents, password)
.context(ParseFileAsPkcs12Snafu)
}
}
}

pub fn path(&self) -> &PathBuf {
match self {
CertInput::Pem(path) => path,
CertInput::Pkcs12(Pkcs12Source { path, .. }) => path,
}
}
}
Loading