Skip to content

Commit 2f4e3e0

Browse files
committed
Support specifing truststore passwords
1 parent bde0ad8 commit 2f4e3e0

4 files changed

Lines changed: 156 additions & 121 deletions

File tree

rust/truststore-merger/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stackable-truststore-merger"
3-
description = "A CLI tool to merge two pkcs12 truststores in such as way that they are accepted by the JVM"
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"
44
version.workspace = true
55
authors.workspace = true
66
license.workspace = true
Lines changed: 44 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,59 @@
1-
use std::{
2-
fs,
3-
io::Write,
4-
path::PathBuf,
5-
process::{Command, Stdio},
6-
};
1+
use std::{fs, path::PathBuf};
72

8-
use anyhow::{Context, bail};
3+
use anyhow::Context;
94
use clap::Parser;
10-
use openssl::{pkcs12::Pkcs12, x509::X509};
11-
use stackable_secret_operator_utils::pem::split_pem_certificates;
5+
use openssl::x509::X509;
6+
7+
use crate::parsers::{parse_pem_contents, parse_pkcs12_file_workaround};
128

139
#[derive(Parser, Debug)]
1410
#[command(version, about)]
1511
pub struct Cli {
16-
/// The path to output the resulting pkcs12 to
12+
/// The path to output the resulting PKCS12 to
1713
#[arg(long)]
1814
pub out: PathBuf,
1915

16+
/// The password used to encrypt the outputted PKCS12 truststore. Defaults to an empty string.
17+
#[arg(long, default_value = "")]
18+
pub out_password: String,
19+
2020
/// List of PEM certificate(s)
2121
#[arg(long = "pem")]
2222
pub pems: Vec<PathBuf>,
2323

24-
/// List of PKCS12 certificate(s)
25-
#[arg(long = "pkcs12")]
26-
pub pkcs12s: Vec<PathBuf>,
24+
/// List of PKCS12 truststore(s)
25+
///
26+
/// You can either use `truststore.p12` (which uses an empty password by default), or specify
27+
/// the password using `truststore.p12:changeit`.
28+
#[arg(long = "pkcs12", value_parser = parse_cli_pkcs12_source)]
29+
pub pkcs12s: Vec<Pkcs12Source>,
30+
}
31+
32+
#[derive(Debug)]
33+
pub enum CertInput {
34+
Pem(PathBuf),
35+
Pkcs12(Pkcs12Source),
36+
}
37+
38+
#[derive(Clone, Debug)]
39+
pub struct Pkcs12Source {
40+
path: PathBuf,
41+
password: String,
42+
}
43+
44+
fn parse_cli_pkcs12_source(cli_argument: &str) -> Result<Pkcs12Source, String> {
45+
let mut parts = cli_argument.splitn(2, ':');
46+
let path = parts
47+
.next()
48+
.ok_or_else(|| "missing path part".to_string())?;
49+
let password = parts.next().unwrap_or("").to_string();
50+
51+
Ok(Pkcs12Source {
52+
path: PathBuf::from(path),
53+
password,
54+
})
2755
}
56+
2857
impl Cli {
2958
pub fn certificate_sources(&self) -> Vec<CertInput> {
3059
let pems = self.pems.iter().cloned().map(CertInput::Pem);
@@ -33,12 +62,6 @@ impl Cli {
3362
}
3463
}
3564

36-
#[derive(Debug)]
37-
pub enum CertInput {
38-
Pem(PathBuf),
39-
Pkcs12(PathBuf),
40-
}
41-
4265
impl CertInput {
4366
pub fn read(&self) -> anyhow::Result<Vec<X509>> {
4467
let file_contents =
@@ -51,8 +74,7 @@ impl CertInput {
5174
path = self.path()
5275
)
5376
}),
54-
CertInput::Pkcs12(_) => {
55-
let password = ""; // TODO
77+
CertInput::Pkcs12(Pkcs12Source { password, .. }) => {
5678
parse_pkcs12_file_workaround(&file_contents, password)
5779
}
5880
}
@@ -61,102 +83,7 @@ impl CertInput {
6183
pub fn path(&self) -> &PathBuf {
6284
match self {
6385
CertInput::Pem(path) => path,
64-
CertInput::Pkcs12(path) => path,
86+
CertInput::Pkcs12(Pkcs12Source { path, .. }) => path,
6587
}
6688
}
6789
}
68-
69-
fn parse_pem_contents(pem_bytes: &[u8]) -> anyhow::Result<Vec<X509>> {
70-
let pems = split_pem_certificates(pem_bytes);
71-
pems.into_iter()
72-
.map(|pem| X509::from_pem(pem).context("failed to parse PEM encoded certificate"))
73-
.collect()
74-
}
75-
76-
/// This function is how we would *should* do it.
77-
///
78-
/// But with legacy old truststores generated by secret-operator (as of 2025-09), this fails with OpenSSL 3 because it
79-
/// removed the old, legacy algorithms:
80-
///
81-
/// `error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:355:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()`
82-
///
83-
/// I tried this code to load the legacy provider:
84-
///
85-
/// ```ignore
86-
/// const LEGACY_PROVIDER_NAME: &str = "legacy";
87-
///
88-
/// unsafe {
89-
/// let provider_name =
90-
/// std::ffi::CString::new(LEGACY_PROVIDER_NAME).expect("constant CString is always valid");
91-
/// let provider = ffi::OSSL_PROVIDER_load(ptr::null_mut(), provider_name.as_ptr());
92-
/// if provider.is_null() {
93-
/// bail!("Failed to load OpenSSL provider {LEGACY_PROVIDER_NAME}");
94-
/// }
95-
/// }
96-
/// ```
97-
///
98-
/// It helped a bit, but we got the next error:
99-
///
100-
/// `error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:375:Global default library context, Algorithm (PKCS12KDF : 0), Properties (<null>), error:1180006B:PKCS12 routines:pkcs12_gen_mac:key gen error:crypto/pkcs12/p12_mutl.c:267:, error:1180006D:PKCS12 routines:PKCS12_verify_mac:mac generation error:crypto/pkcs12/p12_mutl.c:331:, error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:375:Global default library context, Algorithm (PKCS12KDF : 0), Properties (<null>), error:1180006B:PKCS12 routines:pkcs12_gen_mac:key gen error:crypto/pkcs12/p12_mutl.c:267:, error:1180006D:PKCS12 routines:PKCS12_verify_mac:mac generation error:crypto/pkcs12/p12_mutl.c:331:, error:11800071:PKCS12 routines:PKCS12_parse:mac verify failure:crypto/pkcs12/p12_kiss.c:67:`
101-
///
102-
/// So I ditched that effort and we are now shelling out to the CLI. Sorry!
103-
/// The proper solution would be that secret-operator writes pkcs12 truststores using modern algorithms.
104-
#[allow(unused)]
105-
fn parse_pkcs12_file(file_contents: &[u8], password: &str) -> anyhow::Result<Vec<X509>> {
106-
let parsed = Pkcs12::from_der(file_contents)
107-
.context("failed to parse PKCS12 DER encoded file")?
108-
.parse2(password)
109-
.context("Failed to parse PKCS12 using the provided password")?;
110-
111-
parsed
112-
.ca
113-
.context("pkcs12 truststore did not contain a CA")?
114-
.into_iter()
115-
.map(Ok)
116-
.collect()
117-
}
118-
119-
/// Workaround for [`parse_pkcs12_file`]. Please read it's documentation for details.
120-
///
121-
/// Yes, I hate it as well...
122-
fn parse_pkcs12_file_workaround(file_contents: &[u8], password: &str) -> anyhow::Result<Vec<X509>> {
123-
let mut child = Command::new("openssl")
124-
.args(&[
125-
"pkcs12",
126-
"-nokeys",
127-
"-password",
128-
&format!("pass:{}", password),
129-
// That's the important part!!!
130-
"-legacy",
131-
])
132-
.stdin(Stdio::piped())
133-
.stdout(Stdio::piped())
134-
.stderr(Stdio::piped())
135-
.spawn()
136-
.context("Failed to spawn openssl process")?;
137-
138-
{
139-
let stdin = child
140-
.stdin
141-
.as_mut()
142-
.context("Failed to open openssl process stdin")?;
143-
stdin
144-
.write_all(file_contents)
145-
.context("Failed to write PKCS12 data to openssl process stdin")?;
146-
}
147-
148-
let output = child
149-
.wait_with_output()
150-
.context("Failed to read openssl process output")?;
151-
if !output.status.success() {
152-
let stderr = String::from_utf8_lossy(&output.stderr);
153-
bail!("openssl process failed with STDERR: {stderr:?}");
154-
}
155-
156-
parse_pem_contents(&output.stdout).with_context(|| {
157-
format!(
158-
"failed to parse openssl process output, which should be PEM. STDOUT: {stdout}?",
159-
stdout = String::from_utf8_lossy(&output.stdout)
160-
)
161-
})
162-
}

rust/truststore-merger/src/main.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use stackable_secret_operator_utils::pkcs12::pkcs12_truststore;
88
use tracing::{info, level_filters::LevelFilter, warn};
99

1010
mod cli_args;
11+
mod parsers;
1112

1213
pub fn main() -> anyhow::Result<()> {
1314
let filter = tracing_subscriber::EnvFilter::builder()
@@ -71,11 +72,12 @@ pub fn main() -> anyhow::Result<()> {
7172
}
7273
}
7374

74-
let pkcs12_truststore_bytes = pkcs12_truststore(certificates.values().map(|c| &**c), "")
75-
.context("failed to create pkcs12 truststore from certificates")?;
75+
let pkcs12_truststore_bytes =
76+
pkcs12_truststore(certificates.values().map(|c| &**c), &cli.out_password)
77+
.context("failed to create PKCS12 truststore from certificates")?;
7678
fs::write(&cli.out, &pkcs12_truststore_bytes).with_context(|| {
7779
format!(
78-
"failed to write to output pkcs12 truststore at {:?}",
80+
"failed to write to output PKCS12 truststore at {:?}",
7981
cli.out
8082
)
8183
})?;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::{
2+
io::Write,
3+
process::{Command, Stdio},
4+
};
5+
6+
use anyhow::{Context, bail};
7+
use openssl::{pkcs12::Pkcs12, x509::X509};
8+
use stackable_secret_operator_utils::pem::split_pem_certificates;
9+
10+
pub fn parse_pem_contents(pem_bytes: &[u8]) -> anyhow::Result<Vec<X509>> {
11+
let pems = split_pem_certificates(pem_bytes);
12+
pems.into_iter()
13+
.map(|pem| X509::from_pem(pem).context("failed to parse PEM encoded certificate"))
14+
.collect()
15+
}
16+
17+
/// This function is how we would *should* do it.
18+
///
19+
/// But with legacy old truststores generated by secret-operator (as of 2025-09), this fails with OpenSSL 3 because it
20+
/// removed the old, legacy algorithms:
21+
///
22+
/// `error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:355:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()`
23+
///
24+
/// I tried this code to load the legacy provider:
25+
///
26+
/// ```ignore
27+
/// const LEGACY_PROVIDER_NAME: &str = "legacy";
28+
///
29+
/// unsafe {
30+
/// let provider_name =
31+
/// std::ffi::CString::new(LEGACY_PROVIDER_NAME).expect("constant CString is always valid");
32+
/// let provider = ffi::OSSL_PROVIDER_load(ptr::null_mut(), provider_name.as_ptr());
33+
/// if provider.is_null() {
34+
/// bail!("Failed to load OpenSSL provider {LEGACY_PROVIDER_NAME}");
35+
/// }
36+
/// }
37+
/// ```
38+
///
39+
/// It helped a bit, but we got the next error:
40+
///
41+
/// `error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:375:Global default library context, Algorithm (PKCS12KDF : 0), Properties (<null>), error:1180006B:PKCS12 routines:pkcs12_gen_mac:key gen error:crypto/pkcs12/p12_mutl.c:267:, error:1180006D:PKCS12 routines:PKCS12_verify_mac:mac generation error:crypto/pkcs12/p12_mutl.c:331:, error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:375:Global default library context, Algorithm (PKCS12KDF : 0), Properties (<null>), error:1180006B:PKCS12 routines:pkcs12_gen_mac:key gen error:crypto/pkcs12/p12_mutl.c:267:, error:1180006D:PKCS12 routines:PKCS12_verify_mac:mac generation error:crypto/pkcs12/p12_mutl.c:331:, error:11800071:PKCS12 routines:PKCS12_parse:mac verify failure:crypto/pkcs12/p12_kiss.c:67:`
42+
///
43+
/// So I ditched that effort and we are now shelling out to the CLI. Sorry!
44+
/// The proper solution would be that secret-operator writes PKCS12 truststores using modern algorithms.
45+
#[allow(unused)]
46+
pub fn parse_pkcs12_file(file_contents: &[u8], password: &str) -> anyhow::Result<Vec<X509>> {
47+
let parsed = Pkcs12::from_der(file_contents)
48+
.context("failed to parse PKCS12 DER encoded file")?
49+
.parse2(password)
50+
.context("Failed to parse PKCS12 using the provided password")?;
51+
52+
parsed
53+
.ca
54+
.context("pkcs12 truststore did not contain a CA")?
55+
.into_iter()
56+
.map(Ok)
57+
.collect()
58+
}
59+
60+
/// Workaround for [`parse_pkcs12_file`]. Please read it's documentation for details.
61+
///
62+
/// Yes, I hate it as well...
63+
pub fn parse_pkcs12_file_workaround(
64+
file_contents: &[u8],
65+
password: &str,
66+
) -> anyhow::Result<Vec<X509>> {
67+
let mut child = Command::new("openssl")
68+
.args(&[
69+
"pkcs12",
70+
"-nokeys",
71+
"-password",
72+
&format!("pass:{}", password),
73+
// That's the important part!!!
74+
"-legacy",
75+
])
76+
.stdin(Stdio::piped())
77+
.stdout(Stdio::piped())
78+
.stderr(Stdio::piped())
79+
.spawn()
80+
.context("Failed to spawn openssl process")?;
81+
82+
{
83+
let stdin = child
84+
.stdin
85+
.as_mut()
86+
.context("Failed to open openssl process stdin")?;
87+
stdin
88+
.write_all(file_contents)
89+
.context("Failed to write PKCS12 data to openssl process stdin")?;
90+
}
91+
92+
let output = child
93+
.wait_with_output()
94+
.context("Failed to read openssl process output")?;
95+
if !output.status.success() {
96+
let stderr = String::from_utf8_lossy(&output.stderr);
97+
bail!("openssl process failed with STDERR: {stderr:?}");
98+
}
99+
100+
parse_pem_contents(&output.stdout).with_context(|| {
101+
format!(
102+
"failed to parse openssl process output, which should be PEM. STDOUT: {stdout}?",
103+
stdout = String::from_utf8_lossy(&output.stdout)
104+
)
105+
})
106+
}

0 commit comments

Comments
 (0)