From 2dcbc44f8ebd6ce1baa58fb5b80035025aba3603 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 13 Feb 2026 16:09:00 +0100 Subject: [PATCH 1/3] chore: Add built crate --- Cargo.lock | 1 + Cargo.nix | 7 +++++++ rust/cert-tools/Cargo.toml | 3 +++ rust/cert-tools/build.rs | 3 +++ rust/cert-tools/src/main.rs | 6 +++++- 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 rust/cert-tools/build.rs diff --git a/Cargo.lock b/Cargo.lock index 5de79b2f..8395b386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,7 @@ dependencies = [ name = "cert-tools" version = "0.0.0-dev" dependencies = [ + "built", "clap", "hex", "openssl", diff --git a/Cargo.nix b/Cargo.nix index 8f908dd7..2e0afd29 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -1219,6 +1219,13 @@ rec { features = [ "env-filter" ]; } ]; + buildDependencies = [ + { + name = "built"; + packageId = "built"; + features = [ "chrono" "git2" ]; + } + ]; }; "cexpr" = rec { diff --git a/rust/cert-tools/Cargo.toml b/rust/cert-tools/Cargo.toml index 1172ce53..ae054d97 100644 --- a/rust/cert-tools/Cargo.toml +++ b/rust/cert-tools/Cargo.toml @@ -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 diff --git a/rust/cert-tools/build.rs b/rust/cert-tools/build.rs new file mode 100644 index 00000000..fa809bfd --- /dev/null +++ b/rust/cert-tools/build.rs @@ -0,0 +1,3 @@ +fn main() { + built::write_built_file().unwrap(); +} diff --git a/rust/cert-tools/src/main.rs b/rust/cert-tools/src/main.rs index ab20a89d..de96316e 100644 --- a/rust/cert-tools/src/main.rs +++ b/rust/cert-tools/src/main.rs @@ -13,12 +13,16 @@ mod cert_ext; mod cli_args; mod parsers; +mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + #[snafu::report] pub fn main() -> Result<(), snafu::Whatever> { let cli = Cli::parse(); // Use `CONSOLE_LOG_LEVEL` to modify the console log level - let _tracing_guard = Tracing::pre_configured("cert-tools", cli.telemetry) + let _tracing_guard = Tracing::pre_configured(built_info::PKG_NAME, cli.telemetry) .init() .whatever_context("failed to initialize tracing")?; From abbd4922ff76b0ea2b67007d4cf27d863ced8721 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 13 Feb 2026 16:16:48 +0100 Subject: [PATCH 2/3] feat: Restructure code, add typed errors --- Cargo.lock | 18 +-- Cargo.nix | 41 +++--- rust/cert-tools/Cargo.toml | 2 +- rust/cert-tools/src/cert_ext.rs | 28 ++-- rust/cert-tools/src/cli.rs | 136 ++++++++++++++++++ rust/cert-tools/src/cli_args.rs | 115 --------------- rust/cert-tools/src/cmds/mod.rs | 1 + rust/cert-tools/src/cmds/pkcs12.rs | 112 +++++++++++++++ rust/cert-tools/src/main.rs | 105 +++----------- rust/cert-tools/src/parsers/mod.rs | 2 + rust/cert-tools/src/parsers/pem.rs | 16 +++ .../src/{parsers.rs => parsers/pkcs12.rs} | 108 +++++++++----- 12 files changed, 406 insertions(+), 278 deletions(-) create mode 100644 rust/cert-tools/src/cli.rs delete mode 100644 rust/cert-tools/src/cli_args.rs create mode 100644 rust/cert-tools/src/cmds/mod.rs create mode 100644 rust/cert-tools/src/cmds/pkcs12.rs create mode 100644 rust/cert-tools/src/parsers/mod.rs create mode 100644 rust/cert-tools/src/parsers/pem.rs rename rust/cert-tools/src/{parsers.rs => parsers/pkcs12.rs} (53%) diff --git a/Cargo.lock b/Cargo.lock index 8395b386..395bba4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2075,9 +2075,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -3834,30 +3834,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.nix b/Cargo.nix index 2e0afd29..45e934c2 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -7044,9 +7044,9 @@ rec { }; "num-conv" = rec { crateName = "num-conv"; - version = "0.1.0"; + version = "0.2.0"; edition = "2021"; - sha256 = "1ndiyg82q73783jq18isi71a7mjh56wxrk52rlvyx0mi5z9ibmai"; + sha256 = "0l4hj7lp8zbb9am4j3p7vlcv47y9bbazinvnxx9zjhiwkibyr5yg"; libName = "num_conv"; authors = [ "Jacob Pratt " @@ -12967,9 +12967,9 @@ rec { }; "time" = rec { crateName = "time"; - version = "0.3.44"; - edition = "2021"; - sha256 = "179awlwb36zly3nmz5h9awai1h4pbf1d83g2pmvlw4v1pgixkrwi"; + version = "0.3.47"; + edition = "2024"; + sha256 = "0b7g9ly2iabrlgizliz6v5x23yq5d6bpp0mqz6407z1s526d8fvl"; authors = [ "Jacob Pratt " "Time contributors" @@ -12995,8 +12995,8 @@ rec { usesDefaultFeatures = false; } { - name = "serde"; - packageId = "serde"; + name = "serde_core"; + packageId = "serde_core"; optional = true; usesDefaultFeatures = false; } @@ -13015,22 +13015,16 @@ rec { name = "num-conv"; packageId = "num-conv"; } - { - name = "serde"; - packageId = "serde"; - usesDefaultFeatures = false; - features = [ "derive" ]; - } { name = "time-macros"; packageId = "time-macros"; } ]; features = { - "alloc" = [ "serde?/alloc" ]; + "alloc" = [ "serde_core?/alloc" ]; "default" = [ "std" ]; "formatting" = [ "dep:itoa" "std" "time-macros?/formatting" ]; - "large-dates" = [ "time-macros?/large-dates" ]; + "large-dates" = [ "time-core/large-dates" "time-macros?/large-dates" ]; "local-offset" = [ "std" "dep:libc" "dep:num_threads" ]; "macros" = [ "dep:time-macros" ]; "parsing" = [ "time-macros?/parsing" ]; @@ -13038,7 +13032,7 @@ rec { "rand" = [ "rand08" "rand09" ]; "rand08" = [ "dep:rand08" "deranged/rand08" ]; "rand09" = [ "dep:rand09" "deranged/rand09" ]; - "serde" = [ "dep:serde" "time-macros?/serde" "deranged/serde" ]; + "serde" = [ "dep:serde_core" "time-macros?/serde" "deranged/serde" ]; "serde-human-readable" = [ "serde" "formatting" "parsing" ]; "serde-well-known" = [ "serde" "formatting" "parsing" ]; "std" = [ "alloc" ]; @@ -13048,21 +13042,22 @@ rec { }; "time-core" = rec { crateName = "time-core"; - version = "0.1.6"; - edition = "2021"; - sha256 = "0sqwhg7n47gbffyr0zhipqcnskxgcgzz1ix8wirqs2rg3my8x1j0"; + version = "0.1.8"; + edition = "2024"; + sha256 = "1jidl426mw48i7hjj4hs9vxgd9lwqq4vyalm4q8d7y4iwz7y353n"; libName = "time_core"; authors = [ "Jacob Pratt " "Time contributors" ]; - + features = { + }; }; "time-macros" = rec { crateName = "time-macros"; - version = "0.2.24"; - edition = "2021"; - sha256 = "1wzb6hnl35856f58cx259q7ijc4c7yis0qsnydvw5n8jbw9b1krh"; + version = "0.2.27"; + edition = "2024"; + sha256 = "058ja265waq275wxvnfwavbz9r1hd4dgwpfn7a1a9a70l32y8w1f"; procMacro = true; libName = "time_macros"; authors = [ diff --git a/rust/cert-tools/Cargo.toml b/rust/cert-tools/Cargo.toml index ae054d97..e6728de4 100644 --- a/rust/cert-tools/Cargo.toml +++ b/rust/cert-tools/Cargo.toml @@ -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 diff --git a/rust/cert-tools/src/cert_ext.rs b/rust/cert-tools/src/cert_ext.rs index 019a0dfc..da24a30e 100644 --- a/rust/cert-tools/src/cert_ext.rs +++ b/rust/cert-tools/src/cert_ext.rs @@ -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; - fn sha256_digest(&self) -> Result; + fn serial_as_hex(&self) -> Result; + fn sha256_digest(&self) -> Result; } impl CertExt for X509 { - fn serial_as_hex(&self) -> Result { + fn serial_as_hex(&self) -> Result { 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 { + fn sha256_digest(&self) -> Result { self.digest(MessageDigest::sha256()) - .whatever_context("failed to get certificate digest") + .context(RetrieveDigestSnafu) } } diff --git a/rust/cert-tools/src/cli.rs b/rust/cert-tools/src/cli.rs new file mode 100644 index 00000000..3a25cc67 --- /dev/null +++ b/rust/cert-tools/src/cli.rs @@ -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, + + /// 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, +} + +#[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 { + 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 { + 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, 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, + } + } +} diff --git a/rust/cert-tools/src/cli_args.rs b/rust/cert-tools/src/cli_args.rs deleted file mode 100644 index 3eb63929..00000000 --- a/rust/cert-tools/src/cli_args.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::{fs, path::PathBuf}; - -use clap::{Parser, Subcommand}; -use openssl::x509::X509; -use snafu::{ResultExt, ensure_whatever}; -use stackable_telemetry::tracing::TelemetryOptions; - -use crate::parsers::{parse_pem_contents, parse_pkcs12_file_workaround}; - -#[derive(Parser, Debug)] -#[command(version, about)] -pub struct Cli { - #[command(subcommand)] - pub command: CliCommand, - - #[command(flatten, next_help_heading = "Tracing options")] - pub telemetry: TelemetryOptions, -} - -#[derive(Subcommand, Debug)] -pub enum CliCommand { - /// Generate PKCS12 truststore files from PEM or PKCS12 files - GeneratePkcs12Truststore(GeneratePkcs12), -} - -#[derive(Parser, Debug)] -pub struct GeneratePkcs12 { - /// 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, - - /// 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 = parse_cli_pkcs12_source)] - pub pkcs12s: Vec, -} - -#[derive(Debug)] -pub enum CertInput { - Pem(PathBuf), - Pkcs12(Pkcs12Source), -} - -#[derive(Clone, Debug)] -pub struct Pkcs12Source { - path: PathBuf, - password: String, -} - -fn parse_cli_pkcs12_source(cli_argument: &str) -> Result { - let mut parts = cli_argument.splitn(2, ':'); - let path = parts - .next() - .ok_or_else(|| "missing path part".to_string())?; - let password = parts.next().unwrap_or("").to_string(); - - Ok(Pkcs12Source { - path: PathBuf::from(path), - password, - }) -} - -impl GeneratePkcs12 { - pub fn certificate_sources(&self) -> Vec { - let pems = self.pems.iter().cloned().map(CertInput::Pem); - let pkcs12s = self.pkcs12s.iter().cloned().map(CertInput::Pkcs12); - pems.chain(pkcs12s).collect() - } -} - -impl CertInput { - pub fn read(&self) -> Result, snafu::Whatever> { - let read_file_fn = |path| { - fs::read(path).with_whatever_context(|_| format!("failed to read from file {self:?}")) - }; - - match self { - CertInput::Pem(path) => { - let file_contents = read_file_fn(path)?; - - let certs = parse_pem_contents(&file_contents).with_whatever_context(|_| { - format!("failed to parse PEM contents from {path:?}",) - })?; - ensure_whatever!( - !certs.is_empty(), - "The PEM file at {path:?} contained no certificates", - ); - - Ok(certs) - } - CertInput::Pkcs12(Pkcs12Source { path, password }) => { - let file_contents = read_file_fn(path)?; - - parse_pkcs12_file_workaround(&file_contents, password) - } - } - } - - pub fn path(&self) -> &PathBuf { - match self { - CertInput::Pem(path) => path, - CertInput::Pkcs12(Pkcs12Source { path, .. }) => path, - } - } -} diff --git a/rust/cert-tools/src/cmds/mod.rs b/rust/cert-tools/src/cmds/mod.rs new file mode 100644 index 00000000..90815b0a --- /dev/null +++ b/rust/cert-tools/src/cmds/mod.rs @@ -0,0 +1 @@ +pub mod pkcs12; diff --git a/rust/cert-tools/src/cmds/pkcs12.rs b/rust/cert-tools/src/cmds/pkcs12.rs new file mode 100644 index 00000000..a8d7c19d --- /dev/null +++ b/rust/cert-tools/src/cmds/pkcs12.rs @@ -0,0 +1,112 @@ +use std::{collections::HashMap, path::PathBuf}; + +use openssl::x509::X509; +use snafu::{ResultExt, Snafu, ensure}; +use stackable_secret_operator_utils::pkcs12::pkcs12_truststore; + +use crate::{cert_ext::CertExt, cli::GeneratePkcs12TruststoreArguments}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display( + "the list of certificate sources can not be empty. provide at least one --pem or --pkcs12" + ))] + NoCertificateSources, + + #[snafu(display("failed to read certifcate source at {path}", path = path.display()))] + ReadCertificate { + source: crate::cli::CertInputError, + path: PathBuf, + }, + + #[snafu(display("failed to retrieve certificate digest"))] + RetrieveCertificateDigest { source: crate::cert_ext::Error }, + + #[snafu(display("failed to retrieve certificate serial number encoded as hex"))] + RetrieveCertificateSerial { source: crate::cert_ext::Error }, + + #[snafu(display("failed to create truststore"))] + CreateTruststore { + source: stackable_secret_operator_utils::pkcs12::TlsToPkcs12Error, + }, + + #[snafu(display("failed to write truststore contents to file at {path}", path = path.display()))] + WriteTruststoreFile { + source: std::io::Error, + path: PathBuf, + }, +} + +pub fn generate_truststore(args: GeneratePkcs12TruststoreArguments) -> Result<(), Error> { + let certificate_sources = args.certificate_sources(); + ensure!(!certificate_sources.is_empty(), NoCertificateSourcesSnafu); + + let certificate_sources = certificate_sources + .iter() + .map(|source| { + let certificate = source.from_file().context(ReadCertificateSnafu { + path: source.path(), + })?; + + Ok((source, certificate)) + }) + .collect::, Error>>()?; + + let mut certificates = HashMap::, X509>::new(); + for (source, certificates_list) in certificate_sources.into_iter() { + tracing::info!(?source, "Importing certificates"); + + for certificate in certificates_list { + let sha256_digest = certificate + .sha256_digest() + .context(RetrieveCertificateDigestSnafu)?; + + let new_serial = certificate + .serial_as_hex() + .context(RetrieveCertificateSerialSnafu)?; + + // Trying to stick to https://opentelemetry.io/docs/specs/semconv/registry/attributes/tls/#tls-attributes + // Converting `Asn1TimeRef` to a ISO 8601 timestamp really sucks, so we omitted that. + if let Some(existing) = certificates.get(&*sha256_digest) { + let existing_serial = existing + .serial_as_hex() + .context(RetrieveCertificateSerialSnafu)?; + + tracing::warn!( + hash.sha256 = hex::encode(sha256_digest).to_uppercase(), + existing.not_before = ?existing.not_before(), + existing.not_after = ?existing.not_after(), + existing.subject = ?existing.subject_name(), + existing.issuer = ?existing.issuer_name(), + existing.serial = ?existing_serial, + new.not_before = ?certificate.not_before(), + new.not_after = ?certificate.not_after(), + new.subject = ?certificate.subject_name(), + new.issuer = ?certificate.issuer_name(), + new.serial = ?new_serial, + ?source, + "Skipped certificate as a cert with the same SHA256 hash was already added", + ); + } else { + tracing::info!( + subject = ?certificate.subject_name(), + issuer = ?certificate.issuer_name(), + not_before = ?certificate.not_before(), + not_after = ?certificate.not_after(), + serial = ?new_serial, + ?source, + "Added certificate" + ); + + certificates.insert(sha256_digest.to_vec(), certificate); + } + } + } + + let pkcs12_truststore_bytes = + pkcs12_truststore(certificates.values().map(|c| &**c), &args.out_password) + .context(CreateTruststoreSnafu)?; + + std::fs::write(&args.out, &pkcs12_truststore_bytes) + .context(WriteTruststoreFileSnafu { path: args.out }) +} diff --git a/rust/cert-tools/src/main.rs b/rust/cert-tools/src/main.rs index de96316e..774a707a 100644 --- a/rust/cert-tools/src/main.rs +++ b/rust/cert-tools/src/main.rs @@ -1,106 +1,41 @@ -use std::{collections::HashMap, fs}; - -use cert_ext::CertExt; use clap::Parser; -use cli_args::{Cli, CliCommand, GeneratePkcs12}; -use openssl::x509::X509; -use snafu::{ResultExt, ensure_whatever}; -use stackable_secret_operator_utils::pkcs12::pkcs12_truststore; +use snafu::{ResultExt, Snafu}; use stackable_telemetry::Tracing; -use tracing::{info, warn}; + +use crate::cli::{Cli, Command}; mod cert_ext; -mod cli_args; +mod cli; +mod cmds; mod parsers; mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } +#[derive(Debug, Snafu)] +enum Error { + #[snafu(display("failed to initialize tracing"))] + InitializeTracing { + source: stackable_telemetry::tracing::Error, + }, + + #[snafu(display("failed to generate PKCS12 truststore"))] + GeneratePkcs12Truststore { source: cmds::pkcs12::Error }, +} + #[snafu::report] -pub fn main() -> Result<(), snafu::Whatever> { +pub fn main() -> Result<(), Error> { let cli = Cli::parse(); // Use `CONSOLE_LOG_LEVEL` to modify the console log level let _tracing_guard = Tracing::pre_configured(built_info::PKG_NAME, cli.telemetry) .init() - .whatever_context("failed to initialize tracing")?; + .context(InitializeTracingSnafu)?; match cli.command { - CliCommand::GeneratePkcs12Truststore(cli_args) => generate_pkcs12_truststore(cli_args)?, - } - - Ok(()) -} - -fn generate_pkcs12_truststore(cli_args: GeneratePkcs12) -> Result<(), snafu::Whatever> { - let certificate_sources = cli_args.certificate_sources(); - ensure_whatever!( - !certificate_sources.is_empty(), - "The list of certificate sources can not be empty. Please provide at least on --pem or --pkcs12." - ); - let certificate_sources = certificate_sources - .iter() - .map(|source| { - let certificate = source.read().with_whatever_context(|_| { - format!( - "failed to read certificate source {path:?}", - path = source.path() - ) - })?; - Ok((source, certificate)) - }) - .collect::, _>>()?; - - let mut certificates = HashMap::, X509>::new(); - for (source, certificates_list) in certificate_sources.into_iter() { - info!(?source, "Importing certificates"); - - for certificate in certificates_list { - let sha256 = certificate.sha256_digest()?; - - // Trying to stick to https://opentelemetry.io/docs/specs/semconv/registry/attributes/tls/#tls-attributes - // Converting `Asn1TimeRef` to a ISO 8601 timestamp really sucks, so we omitted that. - if let Some(existing) = certificates.get(&*sha256) { - warn!( - ?source, - hash.sha256 = hex::encode(sha256).to_uppercase(), - existing.not_before = ?existing.not_before(), - existing.not_after = ?existing.not_after(), - existing.subject = ?existing.subject_name(), - existing.issuer = ?existing.issuer_name(), - existing.serial = ?existing.serial_as_hex()?, - new.not_before = ?certificate.not_before(), - new.not_after = ?certificate.not_after(), - new.subject = ?certificate.subject_name(), - new.issuer = ?certificate.issuer_name(), - new.serial = ?existing.serial_as_hex()?, - "Skipped certificate as a cert with the same SHA256 hash was already added", - ); - } else { - info!( - subject = ?certificate.subject_name(), - issuer = ?certificate.issuer_name(), - not_before = ?certificate.not_before(), - not_after = ?certificate.not_after(), - serial = ?certificate.serial_as_hex()?, - ?source, - "Added certificate" - ); - certificates.insert(sha256.to_vec(), certificate); - } + Command::GeneratePkcs12Truststore(args) => { + cmds::pkcs12::generate_truststore(args).context(GeneratePkcs12TruststoreSnafu) } } - - let pkcs12_truststore_bytes = - pkcs12_truststore(certificates.values().map(|c| &**c), &cli_args.out_password) - .whatever_context("failed to create PKCS12 truststore from certificates")?; - fs::write(&cli_args.out, &pkcs12_truststore_bytes).with_whatever_context(|_| { - format!( - "failed to write to output PKCS12 truststore at {:?}", - cli_args.out - ) - })?; - - Ok(()) } diff --git a/rust/cert-tools/src/parsers/mod.rs b/rust/cert-tools/src/parsers/mod.rs new file mode 100644 index 00000000..445db9ff --- /dev/null +++ b/rust/cert-tools/src/parsers/mod.rs @@ -0,0 +1,2 @@ +pub mod pem; +pub mod pkcs12; diff --git a/rust/cert-tools/src/parsers/pem.rs b/rust/cert-tools/src/parsers/pem.rs new file mode 100644 index 00000000..ced2631e --- /dev/null +++ b/rust/cert-tools/src/parsers/pem.rs @@ -0,0 +1,16 @@ +use openssl::x509::X509; +use snafu::{ResultExt, Snafu}; +use stackable_secret_operator_utils::pem::split_pem_certificates; + +#[derive(Debug, Snafu)] +#[snafu(display("failed to parse bytes as PEM"))] +pub struct ParseError { + source: openssl::error::ErrorStack, +} + +pub fn parse_contents(pem_bytes: &[u8]) -> Result, ParseError> { + let pems = split_pem_certificates(pem_bytes); + pems.into_iter() + .map(|pem| X509::from_pem(pem).context(ParseSnafu)) + .collect() +} diff --git a/rust/cert-tools/src/parsers.rs b/rust/cert-tools/src/parsers/pkcs12.rs similarity index 53% rename from rust/cert-tools/src/parsers.rs rename to rust/cert-tools/src/parsers/pkcs12.rs index 57bdee41..d3ee4486 100644 --- a/rust/cert-tools/src/parsers.rs +++ b/rust/cert-tools/src/parsers/pkcs12.rs @@ -4,14 +4,18 @@ use std::{ }; use openssl::{pkcs12::Pkcs12, x509::X509}; -use snafu::{OptionExt, ResultExt, whatever}; -use stackable_secret_operator_utils::pem::split_pem_certificates; +use snafu::{OptionExt, ResultExt, Snafu}; -pub fn parse_pem_contents(pem_bytes: &[u8]) -> Result, snafu::Whatever> { - let pems = split_pem_certificates(pem_bytes); - pems.into_iter() - .map(|pem| X509::from_pem(pem).whatever_context("failed to parse PEM encoded certificate")) - .collect() +#[derive(Debug, Snafu)] +pub enum ParseError { + #[snafu(display("failed to deseralize PKCS#12 DER encoded file"))] + DeserializeFile { source: openssl::error::ErrorStack }, + + #[snafu(display("failed to parse file as PKCS#12"))] + ParseFile { source: openssl::error::ErrorStack }, + + #[snafu(display("the PKCS#12 truststore contains no certificate authority"))] + NoCertificateAuthority, } /// This function is how we would *should* do it. @@ -44,30 +48,50 @@ pub fn parse_pem_contents(pem_bytes: &[u8]) -> Result, snafu::Whatever /// The proper solution would be that secret-operator writes PKCS12 truststores using modern algorithms. /// For that we probably(?) drop the p12 crate? #[allow(unused)] -pub fn parse_pkcs12_file( - file_contents: &[u8], - password: &str, -) -> Result, snafu::Whatever> { +pub fn parse_file(file_contents: &[u8], password: &str) -> Result, ParseError> { let parsed = Pkcs12::from_der(file_contents) - .whatever_context("failed to parse PKCS12 DER encoded file")? + .context(DeserializeFileSnafu)? .parse2(password) - .whatever_context("Failed to parse PKCS12 using the provided password")?; + .context(ParseFileSnafu)?; parsed .ca - .whatever_context("pkcs12 truststore did not contain a CA")? + .context(NoCertificateAuthoritySnafu)? .into_iter() .map(Ok) .collect() } +#[derive(Debug, Snafu)] +pub enum WorkaroundError { + #[snafu(display("failed to spawn openssl process"))] + SpawnProcess { source: std::io::Error }, + + #[snafu(display("failed to acquire openssl process stdin handle"))] + AcquireStdinHandle, + + #[snafu(display("failed to write data to openssl process stdin"))] + WriteToStdin { source: std::io::Error }, + + #[snafu(display("failed to wait for openssl process to complete"))] + WaitForProcess { source: std::io::Error }, + + #[snafu(display("the openssl process failed to complete successfully: {reason:?}"))] + ProcessFailed { reason: String }, + + #[snafu(display("failed to parse openssl process stdout as PEM"))] + ParseOutput { + source: crate::parsers::pem::ParseError, + }, +} + /// Workaround for [`parse_pkcs12_file`]. Please read it's documentation for details. /// /// Yes, I hate it as well... -pub fn parse_pkcs12_file_workaround( +pub fn parse_file_workaround( file_contents: &[u8], password: &str, -) -> Result, snafu::Whatever> { +) -> Result, WorkaroundError> { let mut child = Command::new("openssl") .args(&[ "pkcs12", @@ -81,30 +105,40 @@ pub fn parse_pkcs12_file_workaround( .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .whatever_context("Failed to spawn openssl process")?; + .context(SpawnProcessSnafu)?; { - let stdin = child - .stdin - .as_mut() - .whatever_context("Failed to open openssl process stdin")?; - stdin - .write_all(file_contents) - .whatever_context("Failed to write PKCS12 data to openssl process stdin")?; + let stdin = child.stdin.as_mut().context(AcquireStdinHandleSnafu)?; + stdin.write_all(file_contents).context(WriteToStdinSnafu)?; } - let output = child - .wait_with_output() - .whatever_context("Failed to read openssl process output")?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - whatever!("openssl process failed with STDERR:\n{stderr}"); - } + let output = child.wait_with_output().context(WaitForProcessSnafu)?; + output.status.success().context(|| ProcessFailedSnafu { + reason: String::from_utf8_lossy(&output.stderr), + })?; + + crate::parsers::pem::parse_contents(&output.stdout).context(ParseOutputSnafu) +} - parse_pem_contents(&output.stdout).with_whatever_context(|_| { - format!( - "failed to parse openssl process output, which should be PEM. STDOUT:\n{stdout}", - stdout = String::from_utf8_lossy(&output.stdout) - ) - }) +trait BoolExt { + fn context(self, context: F) -> Result<(), E> + where + F: FnOnce() -> C, + C: snafu::IntoError, + E: std::error::Error + snafu::ErrorCompat; +} + +impl BoolExt for bool { + fn context(self, context: F) -> Result<(), E> + where + F: FnOnce() -> C, + C: snafu::IntoError, + E: std::error::Error + snafu::ErrorCompat, + { + if self { + Ok(()) + } else { + Err(context().into_error(snafu::NoneError)) + } + } } From dbf08134124a44aaf5848ac0feead86f8233ae62 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 13 Feb 2026 16:17:31 +0100 Subject: [PATCH 3/3] chore: Add cargo alias --- .cargo/config.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..60e07a5f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +cert-tools = ["run", "-p", "cert-tools", "--"]