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 ;
94use 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) ]
1511pub 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+
2857impl 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-
4265impl 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- }
0 commit comments