1
0
mirror of https://github.com/pcvolkmer/cert-tools.git synced 2025-07-01 05:52:55 +00:00

feat: support for PKCS#12 export

This commit is contained in:
2025-05-01 12:43:01 +02:00
parent 5029376529
commit be4af05507
3 changed files with 175 additions and 16 deletions

View File

@ -1,7 +1,7 @@
[package] [package]
name = "cert-tools" name = "cert-tools"
description = "Application to show, check and cleanup content of PEM files" description = "Application to show, check and cleanup content of PEM files"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
authors = ["Paul-Christian Volkmer"] authors = ["Paul-Christian Volkmer"]

View File

@ -21,8 +21,11 @@ use itertools::Itertools;
use openssl::asn1::Asn1Time; use openssl::asn1::Asn1Time;
use openssl::hash::MessageDigest; use openssl::hash::MessageDigest;
use openssl::nid::Nid; use openssl::nid::Nid;
use openssl::pkcs12::Pkcs12;
use openssl::pkcs7::Pkcs7; use openssl::pkcs7::Pkcs7;
use openssl::pkey::{PKey, PKeyRef, Public}; use openssl::pkey::{PKey, PKeyRef, Private, Public};
use openssl::rsa::Rsa;
use openssl::stack::Stack;
use openssl::x509::X509; use openssl::x509::X509;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::Display; use std::fmt::Display;
@ -50,7 +53,35 @@ fn asn1time(time: &SystemTime) -> Asn1Time {
.unwrap() .unwrap()
} }
#[derive(PartialEq)] pub fn save_p12_file(path: &Path, certs: &Vec<Certificate>, password: &str, private_key: Option<PrivateKey>) -> Result<(), String> {
if certs.is_empty() {
return Err("Invalid chain".to_owned());
}
let mut pkcs12_builder = Pkcs12::builder();
pkcs12_builder.cert(&certs[0].cert);
if certs.len() > 1 {
let mut ca_stack = Stack::<X509>::new().map_err(|_| "Invalid chain".to_owned())?;
certs[1..].iter().for_each(|cert| {
let _ = ca_stack.push(cert.clone().cert);
});
pkcs12_builder.ca(ca_stack);
}
if let Some(private_key) = private_key {
let key = &PKey::from_rsa(private_key.key).map_err(|_| "Invalid key".to_owned())?;
pkcs12_builder.pkey(key);
}
let result = pkcs12_builder.build2(password).map_err(|e| e.to_string())?;
let result = result.to_der().map_err(|e| e.to_string())?;
fs::write(path, result).map_err(|e| e.to_string())?;
Ok(())
}
#[derive(Clone, PartialEq)]
pub enum StringValue { pub enum StringValue {
Valid(String), Valid(String),
Invalid, Invalid,
@ -76,7 +107,9 @@ impl From<String> for StringValue {
} }
} }
#[derive(Clone)]
pub struct PrivateKey { pub struct PrivateKey {
key: Rsa<Private>,
modulus: StringValue, modulus: StringValue,
} }
@ -87,6 +120,7 @@ impl PrivateKey {
match key.rsa() { match key.rsa() {
Ok(key) => Ok(PrivateKey { Ok(key) => Ok(PrivateKey {
key: key.clone(),
modulus: hex_encode(key.n().to_vec()).into(), modulus: hex_encode(key.n().to_vec()).into(),
}), }),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),

View File

@ -19,7 +19,7 @@
#![windows_subsystem = "windows"] #![windows_subsystem = "windows"]
use cert_tools::{Chain, PrivateKey}; use cert_tools::{save_p12_file, Chain, PrivateKey};
use iced::border::Radius; use iced::border::Radius;
use iced::widget::text_editor::{default, Content, Status}; use iced::widget::text_editor::{default, Content, Status};
use iced::widget::{ use iced::widget::{
@ -41,7 +41,8 @@ fn main() -> iced::Result {
..Settings::default() ..Settings::default()
}) })
.window(window::Settings { .window(window::Settings {
icon: window::icon::from_file_data(include_bytes!("../../resources/icon.ico"), None).ok(), icon: window::icon::from_file_data(include_bytes!("../../resources/icon.ico"), None)
.ok(),
..window::Settings::default() ..window::Settings::default()
}) })
.resizable(false) .resizable(false)
@ -65,6 +66,7 @@ impl File {
enum UiMode { enum UiMode {
CertList, CertList,
Output, Output,
Passphrase,
} }
struct Ui { struct Ui {
@ -79,6 +81,9 @@ struct Ui {
status: String, status: String,
chain_indicator_state: IndicatorState, chain_indicator_state: IndicatorState,
key_indicator_state: IndicatorState, key_indicator_state: IndicatorState,
password_1: String,
password_2: String,
} }
impl Ui { impl Ui {
@ -95,6 +100,8 @@ impl Ui {
status: String::new(), status: String::new(),
chain_indicator_state: IndicatorState::Unknown, chain_indicator_state: IndicatorState::Unknown,
key_indicator_state: IndicatorState::Unknown, key_indicator_state: IndicatorState::Unknown,
password_1: String::new(),
password_2: String::new(),
}, },
Task::none(), Task::none(),
) )
@ -250,6 +257,55 @@ impl Ui {
} }
Task::none() Task::none()
} }
Message::AskForPassword => {
self.mode = UiMode::Passphrase;
Task::none()
}
Message::PickExportP12File => {
Task::perform(export_p12_file(), Message::ExportToP12File)
}
Message::ExportToP12File(file) => {
let private_key = match &self.key_file {
File::PrivateKey(_, key) => Some(key.as_ref().clone()),
_ => None,
};
match &self.chain {
None => {}
Some(ref chain) => match file {
Ok(file) => {
match save_p12_file(&file, chain.certs(), &self.password_1, private_key)
{
Ok(_) => {}
Err(err) => {
self.status = format!("{:?}", err);
}
}
}
Err(err) => {
self.status = format!("{:?}", err);
}
},
};
self.password_1 = String::new();
self.password_2 = String::new();
Task::none()
}
Message::SetPw1(pw) => {
self.mode = UiMode::Passphrase;
self.password_1 = pw.clone();
Task::none()
}
Message::SetPw2(pw) => {
self.mode = UiMode::Passphrase;
self.password_2 = pw.clone();
Task::none()
}
Message::Abort => {
self.mode = UiMode::CertList;
self.password_1 = String::new();
self.password_2 = String::new();
Task::none()
}
} }
} }
@ -357,6 +413,15 @@ impl Ui {
.on_press(Message::PickExportFile) .on_press(Message::PickExportFile)
.style(button::primary) .style(button::primary)
}; };
let export_p12_button = if (self.chain_indicator_state == IndicatorState::Success
|| self.chain_indicator_state == IndicatorState::Cleaned) && self.key_indicator_state == IndicatorState::Success
{
button("Export PKCS #12")
.on_press(Message::AskForPassword)
.style(button::primary)
} else {
button("Export PKCS #12").style(button::primary)
};
let clip_button = if self.output.text().trim().is_empty() { let clip_button = if self.output.text().trim().is_empty() {
button("Copy to Clipboard").style(button::secondary) button("Copy to Clipboard").style(button::secondary)
} else { } else {
@ -382,6 +447,7 @@ impl Ui {
.on_press(Message::PrintPem) .on_press(Message::PrintPem)
.style(button::primary), .style(button::primary),
export_button, export_button,
export_p12_button,
text(" "), text(" "),
clip_button, clip_button,
cleanup_button, cleanup_button,
@ -392,6 +458,7 @@ impl Ui {
button("Print information").style(button::primary), button("Print information").style(button::primary),
button("Print PEM").style(button::primary), button("Print PEM").style(button::primary),
export_button, export_button,
export_p12_button,
text(" "), text(" "),
clip_button, clip_button,
cleanup_button, cleanup_button,
@ -497,7 +564,13 @@ impl Ui {
if idx == 0 { if idx == 0 {
container(text("")) container(text(""))
} else { } else {
container(text(format!("{}", idx)).size(10)).padding(1).center_x(24).center_y(14).style(move |_| self.get_cert_key_number_style(idx as u8 - 1, false)) container(text(format!("{}", idx)).size(10))
.padding(1)
.center_x(24)
.center_y(14)
.style(move |_| {
self.get_cert_key_number_style(idx as u8 - 1, false)
})
} }
], ],
row![ row![
@ -507,7 +580,13 @@ impl Ui {
if idx >= chain.certs().len() - 1 { if idx >= chain.certs().len() - 1 {
container(text("")) container(text(""))
} else { } else {
container(text(format!("{}", idx+1)).size(10)).padding(1).center_x(24).center_y(14).style(move |_| self.get_cert_key_number_style(idx as u8, true)) container(text(format!("{}", idx + 1)).size(10))
.padding(1)
.center_x(24)
.center_y(14)
.style(move |_| {
self.get_cert_key_number_style(idx as u8, true)
})
} }
], ],
if cert.dns_names().is_empty() { if cert.dns_names().is_empty() {
@ -680,6 +759,32 @@ impl Ui {
.center_y(80) .center_y(80)
}; };
let ask_for_password = {
let ok_button = if !self.password_1.is_empty() && self.password_1 == self.password_2 {
button("OK").on_press(Message::PickExportP12File)
} else {
button("OK")
};
row![container(
column![
text("Bitte Passwort für den Export eingeben"),
text_input("", &self.password_1)
.secure(true)
.on_input(|t| Message::SetPw1(t)),
text_input("", &self.password_2)
.secure(true)
.on_input(|t| Message::SetPw2(t)),
row![ok_button, button("Cancel").on_press(Message::Abort)].spacing(4),
]
.spacing(4)
.height(Length::Fill)
.width(320),
)
.center_x(320)]
.height(Length::Fill)
.width(Length::Fill)
};
column![ column![
row![ row![
container(column![cert_file_input, ca_file_input, key_file_input].spacing(2)) container(column![cert_file_input, ca_file_input, key_file_input].spacing(2))
@ -693,12 +798,15 @@ impl Ui {
match self.mode { match self.mode {
UiMode::CertList => column![certs, chain_info], UiMode::CertList => column![certs, chain_info],
UiMode::Output => column![output], UiMode::Output => column![output],
UiMode::Passphrase => column![ask_for_password],
}, },
horizontal_rule(1), horizontal_rule(1),
row![ row![
text(&self.status), text(&self.status),
horizontal_space(), horizontal_space(),
text(format!("Version {}", env!("CARGO_PKG_VERSION"))).style(|_| text::Style { color: Some(color!(0x888888)) }), text(format!("Version {}", env!("CARGO_PKG_VERSION"))).style(|_| text::Style {
color: Some(color!(0x888888))
}),
] ]
] ]
.padding(4) .padding(4)
@ -839,11 +947,14 @@ Authority-Key-Id: {}
fn wrong_chain_certificate_indexes(&self) -> Vec<u8> { fn wrong_chain_certificate_indexes(&self) -> Vec<u8> {
if let Some(chain) = &self.chain { if let Some(chain) = &self.chain {
let authority_key_ids = chain.certs().iter() let authority_key_ids = chain
.certs()
.iter()
.map(|cert| cert.authority_key_id().to_string()) .map(|cert| cert.authority_key_id().to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let x = chain.certs()[1..].iter() let x = chain.certs()[1..]
.iter()
.map(|cert| cert.subject_key_id().to_string()) .map(|cert| cert.subject_key_id().to_string())
.enumerate() .enumerate()
.filter_map(|(idx, key_id)| { .filter_map(|(idx, key_id)| {
@ -852,7 +963,8 @@ Authority-Key-Id: {}
} else { } else {
Some(idx as u8) Some(idx as u8)
} }
}).collect::<Vec<_>>(); })
.collect::<Vec<_>>();
return x; return x;
} }
vec![] vec![]
@ -865,11 +977,7 @@ Authority-Key-Id: {}
color!(0x00aa00, 0.2) color!(0x00aa00, 0.2)
}; };
let background = if !fill { let background = if !fill { Color::WHITE } else { background };
Color::WHITE
} else {
background
};
let color = if self.wrong_chain_certificate_indexes().contains(&idx) { let color = if self.wrong_chain_certificate_indexes().contains(&idx) {
color!(0xaa0000) color!(0xaa0000)
@ -907,6 +1015,12 @@ enum Message {
Cleanup, Cleanup,
PickExportFile, PickExportFile,
ExportToFile(Result<PathBuf, Error>), ExportToFile(Result<PathBuf, Error>),
PickExportP12File,
ExportToP12File(Result<PathBuf, Error>),
AskForPassword,
SetPw1(String),
SetPw2(String),
Abort,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -942,3 +1056,14 @@ async fn export_file() -> Result<PathBuf, Error> {
Ok(path.into()) Ok(path.into())
} }
async fn export_p12_file() -> Result<PathBuf, Error> {
let path = rfd::AsyncFileDialog::new()
.set_title("Export file...")
.add_filter("PKCS#12-File", &["p12", "pfx"])
.save_file()
.await
.ok_or(Error::Undefined)?;
Ok(path.into())
}