Skip to content

Commit b07e640

Browse files
Techassiadwk67
andauthored
feat(cert-tools): Add typed errors and restructure code (#678)
* chore: Add built crate * feat: Restructure code, add typed errors * chore: Add cargo alias * chore: Adjust variable name * chore: Adjust dev comment about TLS semantic conventions Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech> --------- Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech>
1 parent 6de8388 commit b07e640

14 files changed

Lines changed: 428 additions & 279 deletions

File tree

.cargo/config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[alias]
2+
cert-tools = ["run", "-p", "cert-tools", "--"]

Cargo.lock

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.nix

Lines changed: 25 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/cert-tools/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cert-tools"
3-
description = "A CLI tool to merge two truststores in PEM or PKCS12 format in such as way that they are accepted by the JVM"
3+
description = "Merge multiple truststores encoded as PEM or PKCS12 into a JVM compatible format"
44
version = "0.0.0-dev"
55
authors.workspace = true
66
license.workspace = true
@@ -18,3 +18,6 @@ openssl.workspace = true
1818
snafu.workspace = true
1919
tracing.workspace = true
2020
tracing-subscriber = { workspace = true, features = ["env-filter"] }
21+
22+
[build-dependencies]
23+
built.workspace = true

rust/cert-tools/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
built::write_built_file().unwrap();
3+
}

rust/cert-tools/src/cert_ext.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,36 @@ use openssl::{
33
string::OpensslString,
44
x509::X509,
55
};
6-
use snafu::ResultExt;
6+
use snafu::{ResultExt, Snafu};
7+
8+
#[derive(Debug, Snafu)]
9+
pub enum Error {
10+
#[snafu(display("failed to convert certificate serial number to BigNum"))]
11+
ConvertSerialToBigNum { source: openssl::error::ErrorStack },
12+
13+
#[snafu(display("failed to convert certificate serial number to a hexadecimal string"))]
14+
ConvertSerialToHexString { source: openssl::error::ErrorStack },
15+
16+
#[snafu(display("failed to retireve certificate digest as SHA256"))]
17+
RetrieveDigest { source: openssl::error::ErrorStack },
18+
}
719

820
pub trait CertExt {
9-
fn serial_as_hex(&self) -> Result<OpensslString, snafu::Whatever>;
10-
fn sha256_digest(&self) -> Result<DigestBytes, snafu::Whatever>;
21+
fn serial_as_hex(&self) -> Result<OpensslString, Error>;
22+
fn sha256_digest(&self) -> Result<DigestBytes, Error>;
1123
}
1224

1325
impl CertExt for X509 {
14-
fn serial_as_hex(&self) -> Result<OpensslString, snafu::Whatever> {
26+
fn serial_as_hex(&self) -> Result<OpensslString, Error> {
1527
self.serial_number()
1628
.to_bn()
17-
.whatever_context("failed to get certificate serial number as BigNumber")?
29+
.context(ConvertSerialToBigNumSnafu)?
1830
.to_hex_str()
19-
.whatever_context("failed to convert certificate serial number to hex string")
31+
.context(ConvertSerialToHexStringSnafu)
2032
}
2133

22-
fn sha256_digest(&self) -> Result<DigestBytes, snafu::Whatever> {
34+
fn sha256_digest(&self) -> Result<DigestBytes, Error> {
2335
self.digest(MessageDigest::sha256())
24-
.whatever_context("failed to get certificate digest")
36+
.context(RetrieveDigestSnafu)
2537
}
2638
}

rust/cert-tools/src/cli.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::{fs, path::PathBuf, str::FromStr};
2+
3+
use clap::{Parser, Subcommand};
4+
use openssl::x509::X509;
5+
use snafu::{OptionExt, ResultExt, Snafu, ensure};
6+
use stackable_telemetry::tracing::TelemetryOptions;
7+
8+
use crate::parsers::{pem, pkcs12};
9+
10+
#[derive(Parser, Debug)]
11+
#[command(version, about)]
12+
pub struct Cli {
13+
#[command(subcommand)]
14+
pub command: Command,
15+
16+
#[command(flatten, next_help_heading = "Tracing options")]
17+
pub telemetry: TelemetryOptions,
18+
}
19+
20+
#[derive(Subcommand, Debug)]
21+
pub enum Command {
22+
/// Generate PKCS12 truststore files from PEM or PKCS12 files
23+
GeneratePkcs12Truststore(GeneratePkcs12TruststoreArguments),
24+
}
25+
26+
#[derive(Parser, Debug)]
27+
pub struct GeneratePkcs12TruststoreArguments {
28+
/// The path to output the resulting PKCS12 to
29+
#[arg(long)]
30+
pub out: PathBuf,
31+
32+
/// The password used to encrypt the outputted PKCS12 truststore. Defaults to an empty string.
33+
#[arg(long, default_value = "")]
34+
pub out_password: String,
35+
36+
/// List of PEM certificate(s)
37+
#[arg(long = "pem")]
38+
pub pems: Vec<PathBuf>,
39+
40+
/// List of PKCS12 truststore(s)
41+
///
42+
/// You can either use `truststore.p12` (which uses an empty password by default), or specify
43+
/// the password using `truststore.p12:changeit`.
44+
#[arg(long = "pkcs12", value_parser = Pkcs12Source::from_str)]
45+
pub pkcs12s: Vec<Pkcs12Source>,
46+
}
47+
48+
#[derive(Debug, Snafu)]
49+
#[snafu(display("missing path"))]
50+
pub struct Pkcs12SourceParseError;
51+
52+
#[derive(Clone, Debug)]
53+
pub struct Pkcs12Source {
54+
path: PathBuf,
55+
password: String,
56+
}
57+
58+
impl FromStr for Pkcs12Source {
59+
type Err = Pkcs12SourceParseError;
60+
61+
fn from_str(s: &str) -> Result<Self, Self::Err> {
62+
let mut parts = s.splitn(2, ':');
63+
let path = parts.next().context(Pkcs12SourceParseSnafu)?;
64+
let password = parts.next().unwrap_or("").to_owned();
65+
66+
Ok(Self {
67+
path: PathBuf::from(path),
68+
password,
69+
})
70+
}
71+
}
72+
73+
impl GeneratePkcs12TruststoreArguments {
74+
pub fn certificate_sources(&self) -> Vec<CertInput> {
75+
let pems = self.pems.iter().cloned().map(CertInput::Pem);
76+
let pkcs12s = self.pkcs12s.iter().cloned().map(CertInput::Pkcs12);
77+
pems.chain(pkcs12s).collect()
78+
}
79+
}
80+
81+
#[derive(Debug, Snafu)]
82+
pub enum CertInputError {
83+
#[snafu(display("failed to read from file at {path}", path = path.display()))]
84+
ReadFile {
85+
source: std::io::Error,
86+
path: PathBuf,
87+
},
88+
89+
#[snafu(display("failed to parse file contents as PEM"))]
90+
ParseFileAsPem {
91+
source: crate::parsers::pem::ParseError,
92+
},
93+
94+
#[snafu(display("failed to parse file contents as PKCS#12"))]
95+
ParseFileAsPkcs12 {
96+
source: crate::parsers::pkcs12::WorkaroundError,
97+
},
98+
99+
#[snafu(display("the PEM file at {path} contained no certificates", path = path.display()))]
100+
NoCertificates { path: PathBuf },
101+
}
102+
103+
#[derive(Debug)]
104+
pub enum CertInput {
105+
Pem(PathBuf),
106+
Pkcs12(Pkcs12Source),
107+
}
108+
109+
impl CertInput {
110+
pub fn from_file(&self) -> Result<Vec<X509>, CertInputError> {
111+
let read_file_fn = |path| fs::read(path).context(ReadFileSnafu { path });
112+
113+
match self {
114+
CertInput::Pem(path) => {
115+
let file_contents = read_file_fn(path)?;
116+
117+
let certs = pem::parse_contents(&file_contents).context(ParseFileAsPemSnafu)?;
118+
ensure!(!certs.is_empty(), NoCertificatesSnafu { path });
119+
120+
Ok(certs)
121+
}
122+
CertInput::Pkcs12(Pkcs12Source { path, password }) => {
123+
let file_contents = read_file_fn(path)?;
124+
pkcs12::parse_file_workaround(&file_contents, password)
125+
.context(ParseFileAsPkcs12Snafu)
126+
}
127+
}
128+
}
129+
130+
pub fn path(&self) -> &PathBuf {
131+
match self {
132+
CertInput::Pem(path) => path,
133+
CertInput::Pkcs12(Pkcs12Source { path, .. }) => path,
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)