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

Merge pull request #7 from pcvolkmer/feat_p12-support

This commit is contained in:
2025-05-01 13:42:53 +02:00
committed by GitHub
3 changed files with 216 additions and 16 deletions

View File

@ -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"]

View File

@ -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<Certificate>, 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(())
}
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<String> for StringValue {
}
}
#[derive(Clone)]
pub struct PrivateKey {
key: Rsa<Private>,
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()),

View File

@ -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<u8> {
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::<Vec<_>>();
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::<Vec<_>>();
})
.collect::<Vec<_>>();
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<PathBuf, Error>),
PickExportP12File,
ExportToP12File(Result<PathBuf, Error>),
AskForPassword,
SetPw1(String),
SetPw2(String),
Abort,
}
#[derive(Debug, Clone)]
@ -942,3 +1056,14 @@ async fn export_file() -> Result<PathBuf, Error> {
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())
}