diff --git a/Cargo.toml b/Cargo.toml index fa87b86..90b17cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cert-tools" description = "Application to show, check and cleanup content of PEM files" -version = "0.3.0" +version = "0.4.0" edition = "2021" license = "GPL-3.0-or-later" authors = ["Paul-Christian Volkmer"] diff --git a/src/lib.rs b/src/lib.rs index 4d316e0..75eceda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,8 +21,11 @@ use itertools::Itertools; use openssl::asn1::Asn1Time; use openssl::hash::MessageDigest; use openssl::nid::Nid; +use openssl::pkcs12::Pkcs12; 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 std::cmp::Ordering; use std::fmt::Display; @@ -50,7 +53,76 @@ fn asn1time(time: &SystemTime) -> Asn1Time { .unwrap() } -#[derive(PartialEq)] +pub fn save_p12_file(path: &Path, password: &str, certs: &Vec, private_key: Option) -> 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::::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(()) +} + +pub fn read_p12_file(path: &Path, password: &str) -> Result<(Chain, PrivateKey), String> { + let file = fs::read(path).map_err(|err| err.to_string())?; + let pkcs12 = Pkcs12::from_der(&file).map_err(|_| "Cannot read file".to_owned())?; + let pkcs12 = pkcs12.parse2(password).map_err(|_| "Wrong password".to_owned())?; + + let mut certs = vec![]; + if let Some(cert) = pkcs12.cert { + let cert = Certificate::from_x509(&cert)?; + certs.push(cert); + } + + if let Some(ca_certs) = pkcs12.ca { + ca_certs.iter().for_each(|cert| { + if let Ok(pem) = cert.to_pem() { + if let Ok(cert) = X509::from_pem(pem.as_slice()) { + let cert = Certificate::from_x509(&cert).unwrap(); + certs.push(cert); + } + } + }); + } + + let pkey = if let Some(key) = pkcs12.pkey { + match key.rsa() { + Ok(key) => Ok(PrivateKey { + key: key.clone(), + modulus: hex_encode(key.n().to_vec()).into(), + }), + Err(err) => Err(err.to_string()), + } + } else { + Err("Cannot read file: Error in private key".to_owned()) + }; + + if certs.is_empty() || pkey.is_err() { + Err("Cannot read file".to_owned()) + } else { + Ok((Chain::from(certs), pkey?)) + } +} + +#[derive(Clone, PartialEq)] pub enum StringValue { Valid(String), Invalid, @@ -76,7 +148,9 @@ impl From for StringValue { } } +#[derive(Clone)] pub struct PrivateKey { + key: Rsa, modulus: StringValue, } @@ -87,6 +161,7 @@ impl PrivateKey { match key.rsa() { Ok(key) => Ok(PrivateKey { + key: key.clone(), modulus: hex_encode(key.n().to_vec()).into(), }), Err(err) => Err(err.to_string()), diff --git a/ui/src/main.rs b/ui/src/main.rs index a14a009..207d2d3 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -19,7 +19,7 @@ #![windows_subsystem = "windows"] -use cert_tools::{Chain, PrivateKey}; +use cert_tools::{save_p12_file, Chain, PrivateKey}; use iced::border::Radius; use iced::widget::text_editor::{default, Content, Status}; use iced::widget::{ @@ -41,7 +41,8 @@ fn main() -> iced::Result { ..Settings::default() }) .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() }) .resizable(false) @@ -65,6 +66,7 @@ impl File { enum UiMode { CertList, Output, + Passphrase, } struct Ui { @@ -79,6 +81,9 @@ struct Ui { status: String, chain_indicator_state: IndicatorState, key_indicator_state: IndicatorState, + + password_1: String, + password_2: String, } impl Ui { @@ -95,6 +100,8 @@ impl Ui { status: String::new(), chain_indicator_state: IndicatorState::Unknown, key_indicator_state: IndicatorState::Unknown, + password_1: String::new(), + password_2: String::new(), }, Task::none(), ) @@ -250,6 +257,55 @@ impl Ui { } 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) .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() { button("Copy to Clipboard").style(button::secondary) } else { @@ -382,6 +447,7 @@ impl Ui { .on_press(Message::PrintPem) .style(button::primary), export_button, + export_p12_button, text(" "), clip_button, cleanup_button, @@ -392,6 +458,7 @@ impl Ui { button("Print information").style(button::primary), button("Print PEM").style(button::primary), export_button, + export_p12_button, text(" "), clip_button, cleanup_button, @@ -497,7 +564,13 @@ impl Ui { if idx == 0 { container(text("")) } 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![ @@ -507,7 +580,13 @@ impl Ui { if idx >= chain.certs().len() - 1 { container(text("")) } 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() { @@ -680,6 +759,32 @@ impl Ui { .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![ row![ container(column![cert_file_input, ca_file_input, key_file_input].spacing(2)) @@ -693,12 +798,15 @@ impl Ui { match self.mode { UiMode::CertList => column![certs, chain_info], UiMode::Output => column![output], + UiMode::Passphrase => column![ask_for_password], }, horizontal_rule(1), row![ text(&self.status), 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) @@ -839,11 +947,14 @@ Authority-Key-Id: {} fn wrong_chain_certificate_indexes(&self) -> Vec { 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()) .collect::>(); - let x = chain.certs()[1..].iter() + let x = chain.certs()[1..] + .iter() .map(|cert| cert.subject_key_id().to_string()) .enumerate() .filter_map(|(idx, key_id)| { @@ -852,7 +963,8 @@ Authority-Key-Id: {} } else { Some(idx as u8) } - }).collect::>(); + }) + .collect::>(); return x; } vec![] @@ -865,11 +977,7 @@ Authority-Key-Id: {} color!(0x00aa00, 0.2) }; - let background = if !fill { - Color::WHITE - } else { - background - }; + let background = if !fill { Color::WHITE } else { background }; let color = if self.wrong_chain_certificate_indexes().contains(&idx) { color!(0xaa0000) @@ -907,6 +1015,12 @@ enum Message { Cleanup, PickExportFile, ExportToFile(Result), + PickExportP12File, + ExportToP12File(Result), + AskForPassword, + SetPw1(String), + SetPw2(String), + Abort, } #[derive(Debug, Clone)] @@ -942,3 +1056,14 @@ async fn export_file() -> Result { Ok(path.into()) } + +async fn export_p12_file() -> Result { + 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()) +}