mirror of
https://github.com/pcvolkmer/cert-tools.git
synced 2025-04-19 09:06:49 +00:00
feat: add iced based GUI
This commit is contained in:
parent
ee8409957c
commit
9847ff3229
@ -6,6 +6,9 @@ edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Paul-Christian Volkmer"]
|
||||
|
||||
[workspace]
|
||||
members = ["ui"]
|
||||
|
||||
[dependencies]
|
||||
openssl = { version = "0.10"}
|
||||
clap = { version = "4.5", features = ["std", "help", "usage", "derive", "error-context"], default-features = false }
|
||||
|
@ -56,4 +56,8 @@ Options:
|
||||
|
||||
```shell
|
||||
cert-tools merge cert.pem ca.pem > chain.pem
|
||||
```
|
||||
```
|
||||
|
||||
## GUI
|
||||
|
||||
In addition to the console-based application, a simple graphical user interface is available in (sub-)package `ui`.
|
10
ui/Cargo.toml
Normal file
10
ui/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "cert-tools-ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cert-tools = { path = "..", version = "*" }
|
||||
iced = { version = "0.13", features = ["tiny-skia"], default-features = false }
|
||||
rfd = "0.15"
|
||||
itertools = "0.14"
|
507
ui/src/main.rs
Normal file
507
ui/src/main.rs
Normal file
@ -0,0 +1,507 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use cert_tools::{Chain, PrivateKey};
|
||||
use iced::widget::text_editor::{default, Content, Status};
|
||||
use iced::widget::{
|
||||
button, column, container, horizontal_rule, horizontal_space, row, text, text_editor,
|
||||
Scrollable,
|
||||
};
|
||||
use iced::{application, clipboard, Background, Color, Element, Font, Length, Size, Task};
|
||||
use itertools::Itertools;
|
||||
use std::cmp::Ordering;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() -> iced::Result {
|
||||
application(Ui::title, Ui::update, Ui::view)
|
||||
.resizable(false)
|
||||
.window_size(Size::new(1024.0, 800.0))
|
||||
.scale_factor(|_| 0.8)
|
||||
.run_with(Ui::new)
|
||||
}
|
||||
|
||||
struct Ui {
|
||||
cert_file: Option<PathBuf>,
|
||||
ca_file: Option<PathBuf>,
|
||||
key_file: Option<PathBuf>,
|
||||
|
||||
output: Content,
|
||||
status: String,
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
(
|
||||
Self {
|
||||
cert_file: None,
|
||||
ca_file: None,
|
||||
key_file: None,
|
||||
output: Content::default(),
|
||||
status: String::new(),
|
||||
},
|
||||
Task::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"CertTools".into()
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::PickCertFile => Task::perform(pick_file(), Message::SetCertFile),
|
||||
Message::PickCaFile => Task::perform(pick_file(), Message::SetCaFile),
|
||||
Message::PickKeyFile => Task::perform(pick_file(), Message::SetKeyFile),
|
||||
Message::ClearCertFile => {
|
||||
self.cert_file = None;
|
||||
self.output = Content::default();
|
||||
Task::none()
|
||||
}
|
||||
Message::ClearCaFile => {
|
||||
self.ca_file = None;
|
||||
self.output = Content::default();
|
||||
Task::none()
|
||||
}
|
||||
Message::ClearKeyFile => {
|
||||
self.key_file = None;
|
||||
self.output = Content::default();
|
||||
Task::none()
|
||||
}
|
||||
Message::SetCertFile(file) => {
|
||||
match file {
|
||||
Ok(file) => {
|
||||
self.cert_file = Some(file);
|
||||
self.output = Content::default();
|
||||
},
|
||||
_ => self.cert_file = None,
|
||||
};
|
||||
Task::none()
|
||||
}
|
||||
Message::SetCaFile(file) => {
|
||||
match file {
|
||||
Ok(file) => {
|
||||
self.ca_file = Some(file);
|
||||
self.output = Content::default();
|
||||
},
|
||||
_ => self.ca_file = None,
|
||||
};
|
||||
Task::none()
|
||||
}
|
||||
Message::SetKeyFile(file) => {
|
||||
match file {
|
||||
Ok(file) => {
|
||||
self.key_file = Some(file);
|
||||
self.output = Content::default();
|
||||
},
|
||||
_ => self.key_file = None,
|
||||
};
|
||||
Task::none()
|
||||
}
|
||||
Message::Print => {
|
||||
match self.print_output() {
|
||||
Ok(output) => {
|
||||
self.output = Content::with_text(output.as_str());
|
||||
self.status = String::new();
|
||||
}
|
||||
Err(err) => {
|
||||
self.output = Content::default();
|
||||
self.status = err
|
||||
}
|
||||
};
|
||||
Task::none()
|
||||
}
|
||||
Message::Merge => {
|
||||
match self.merge_output() {
|
||||
Ok(output) => {
|
||||
self.output = Content::with_text(output.as_str());
|
||||
self.status = String::new();
|
||||
}
|
||||
Err(err) => {
|
||||
self.output = Content::default();
|
||||
self.status = err
|
||||
}
|
||||
};
|
||||
Task::none()
|
||||
}
|
||||
Message::Copy => clipboard::write::<Message>(self.output.text()),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
fn grey_out_style(is_active: bool) -> text::Style {
|
||||
text::Style {
|
||||
color: if is_active {
|
||||
Some(Color::BLACK)
|
||||
} else {
|
||||
Some(Color::parse("#888888").unwrap())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let cert_file_input = {
|
||||
row![
|
||||
text("Certificate: ").width(100),
|
||||
text(match self.cert_file {
|
||||
Some(ref file) => file.display().to_string(),
|
||||
_ => "No certificate file".to_string(),
|
||||
})
|
||||
.style(|_| grey_out_style(self.cert_file.is_some())),
|
||||
horizontal_space(),
|
||||
if self.cert_file.is_some() {
|
||||
button("x")
|
||||
.on_press(Message::ClearCertFile)
|
||||
.style(button::danger)
|
||||
} else {
|
||||
button("x").style(button::danger)
|
||||
},
|
||||
button("..")
|
||||
.on_press(Message::PickCertFile)
|
||||
.style(button::secondary)
|
||||
]
|
||||
.spacing(2)
|
||||
};
|
||||
|
||||
let ca_file_input = {
|
||||
row![
|
||||
text("CA: ").width(100),
|
||||
text(match self.ca_file {
|
||||
Some(ref file) => file.display().to_string(),
|
||||
_ => "No CA file".to_string(),
|
||||
})
|
||||
.style(|_| grey_out_style(self.ca_file.is_some())),
|
||||
horizontal_space(),
|
||||
if self.ca_file.is_some() {
|
||||
button("x")
|
||||
.on_press(Message::ClearCaFile)
|
||||
.style(button::danger)
|
||||
} else {
|
||||
button("x").style(button::danger)
|
||||
},
|
||||
if self.cert_file.is_some() {
|
||||
button("..")
|
||||
.on_press(Message::PickCaFile)
|
||||
.style(button::secondary)
|
||||
} else {
|
||||
button("..").style(button::secondary)
|
||||
}
|
||||
]
|
||||
.spacing(2)
|
||||
};
|
||||
|
||||
let key_file_input = {
|
||||
row![
|
||||
text("Key: ").width(100),
|
||||
text(match self.key_file {
|
||||
Some(ref file) => file.display().to_string(),
|
||||
_ => "No key file".to_string(),
|
||||
})
|
||||
.style(|_| grey_out_style(self.key_file.is_some())),
|
||||
horizontal_space(),
|
||||
if self.key_file.is_some() {
|
||||
button("x")
|
||||
.on_press(Message::ClearKeyFile)
|
||||
.style(button::danger)
|
||||
} else {
|
||||
button("x").style(button::danger)
|
||||
},
|
||||
if self.cert_file.is_some() {
|
||||
button("..")
|
||||
.on_press(Message::PickKeyFile)
|
||||
.style(button::secondary)
|
||||
} else {
|
||||
button("..").style(button::secondary)
|
||||
}
|
||||
]
|
||||
.spacing(2)
|
||||
};
|
||||
|
||||
let clip_button = if self.output.text().trim().is_empty() {
|
||||
button("Copy to Clipboard").style(button::secondary)
|
||||
} else {
|
||||
button("Copy to Clipboard")
|
||||
.on_press(Message::Copy)
|
||||
.style(button::secondary)
|
||||
};
|
||||
let buttons = if self.cert_file.is_some() {
|
||||
row![
|
||||
button("Print information")
|
||||
.on_press(Message::Print)
|
||||
.style(button::primary),
|
||||
button("Merge into PEM")
|
||||
.on_press(Message::Merge)
|
||||
.style(button::primary),
|
||||
text(" "),
|
||||
clip_button,
|
||||
horizontal_space(),
|
||||
]
|
||||
} else {
|
||||
row![
|
||||
button("Print information").style(button::primary),
|
||||
button("Merge into PEM").style(button::primary),
|
||||
text(" "),
|
||||
clip_button,
|
||||
horizontal_space(),
|
||||
]
|
||||
}
|
||||
.spacing(2);
|
||||
|
||||
let output = {
|
||||
Scrollable::new(
|
||||
text_editor(&self.output)
|
||||
.style(|theme, _| text_editor::Style {
|
||||
background: Background::Color(Color::BLACK),
|
||||
value: Color::WHITE,
|
||||
..default(theme, Status::Disabled)
|
||||
})
|
||||
.font(Font::MONOSPACE)
|
||||
)
|
||||
.height(Length::Fill)
|
||||
};
|
||||
|
||||
let indicator = {
|
||||
let content = match self.indicator_state() {
|
||||
IndicatorState::Unknown => ("?", "#aaaaaa", "#ffffff"),
|
||||
IndicatorState::Success => ("OK", "#00aa00", "#ffffff"),
|
||||
IndicatorState::Error => ("Not OK", "#aa0000", "#ffffff"),
|
||||
};
|
||||
|
||||
container(
|
||||
container(text(content.0))
|
||||
.style(|_| container::Style {
|
||||
background: Some(Background::Color(Color::parse(content.1).unwrap())),
|
||||
text_color: Some(Color::parse(content.2).unwrap()),
|
||||
..container::Style::default()
|
||||
})
|
||||
.center_x(80)
|
||||
.center_y(80),
|
||||
)
|
||||
.center_x(96)
|
||||
.center_y(96)
|
||||
};
|
||||
|
||||
column![
|
||||
row![
|
||||
column![cert_file_input, ca_file_input, key_file_input].spacing(2),
|
||||
indicator,
|
||||
]
|
||||
.spacing(96),
|
||||
horizontal_rule(1),
|
||||
buttons,
|
||||
output,
|
||||
horizontal_rule(1),
|
||||
text(&self.status)
|
||||
]
|
||||
.padding(4)
|
||||
.spacing(2)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn print_output(&self) -> Result<String, String> {
|
||||
let mut output = vec![];
|
||||
if let Some(cert_file) = &self.cert_file {
|
||||
let chain = Chain::read(cert_file);
|
||||
|
||||
if let Ok(mut chain) = chain {
|
||||
if let Some(ca_file) = &self.ca_file {
|
||||
if let Ok(ca_chain) = Chain::read(ca_file) {
|
||||
for ca_cert in ca_chain.into_vec() {
|
||||
chain.push(ca_cert);
|
||||
}
|
||||
} else {
|
||||
return Err("Cannot read CA file".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
for cert in chain.certs() {
|
||||
let s = format!(
|
||||
"Name: {}
|
||||
Issuer: {}
|
||||
Gültigkeit: Gültig von: {} bis: {}
|
||||
SHA-1-Fingerprint: {}
|
||||
SHA-256-Fingerprint: {}
|
||||
Subject-Key-Id: {}
|
||||
Authority-Key-Id: {}
|
||||
{}",
|
||||
cert.name(),
|
||||
cert.issuer(),
|
||||
cert.not_before(),
|
||||
cert.not_after(),
|
||||
cert.fingerprint().sha1,
|
||||
cert.fingerprint().sha256,
|
||||
cert.subject_key_id(),
|
||||
cert.authority_key_id(),
|
||||
if cert.dns_names().is_empty() {
|
||||
"\n".to_string()
|
||||
} else {
|
||||
format!("DNS-Names: {}\n", cert.dns_names().join(", "))
|
||||
}
|
||||
);
|
||||
output.push(s);
|
||||
}
|
||||
|
||||
if chain.has_missing_tail() {
|
||||
output.push("! Last Certificate points to another one that should be contained in chain.".to_string());
|
||||
output.push(" Self signed (CA-) Certificate? It might be required to import a self signed Root-CA manually for applications to use it.".to_string());
|
||||
}
|
||||
|
||||
if chain.is_valid() {
|
||||
output.push("✓ Chain is valid".to_string());
|
||||
} else {
|
||||
output.push("! Chain or some of its parts is not valid (anymore)".to_string());
|
||||
}
|
||||
|
||||
if let Some(key) = &self.key_file {
|
||||
match PrivateKey::read(Path::new(&key)) {
|
||||
Ok(private_key) => {
|
||||
if let Some(cert) = chain.certs().first() {
|
||||
if cert.public_key_matches(private_key) {
|
||||
output.push(
|
||||
"✓ Private Key matches first Cert Public Key".to_string(),
|
||||
);
|
||||
} else {
|
||||
output.push(
|
||||
"! Private Key does not match the first Cert Public Key"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err("Could not read Private Key".to_string()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Cannot read Certificate file".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output.join("\n"))
|
||||
}
|
||||
|
||||
fn merge_output(&self) -> Result<String, String> {
|
||||
let mut result = String::new();
|
||||
if let Some(cert_file) = &self.cert_file {
|
||||
let chain = Chain::read(cert_file);
|
||||
|
||||
if let Ok(mut chain) = chain {
|
||||
if let Some(ca_file) = &self.ca_file {
|
||||
if let Ok(ca_chain) = Chain::read(ca_file) {
|
||||
for ca_cert in ca_chain.into_vec() {
|
||||
chain.push(ca_cert);
|
||||
}
|
||||
} else {
|
||||
return Err("Cannot read CA file".to_string());
|
||||
}
|
||||
}
|
||||
let mut certs = chain.into_vec();
|
||||
certs.sort_by(|cert1, cert2| {
|
||||
if cert1.subject_key_id() == cert2.authority_key_id() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Less
|
||||
}
|
||||
});
|
||||
let chain = Chain::from(certs.into_iter().unique().collect::<Vec<_>>());
|
||||
if !chain.is_valid() {
|
||||
return Err("Cannot merge files to valid chain - giving up!".to_string());
|
||||
}
|
||||
for cert in chain.certs() {
|
||||
match cert.to_pem() {
|
||||
Ok(plain) => result.push_str(&plain),
|
||||
Err(_) => {
|
||||
return Err(
|
||||
"Cannot merge files to valid chain - Cert error!".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Cannot read Certificate file".to_string());
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn indicator_state(&self) -> IndicatorState {
|
||||
let mut result = IndicatorState::Unknown;
|
||||
|
||||
if let Some(cert_file) = &self.cert_file {
|
||||
let chain = Chain::read(cert_file);
|
||||
|
||||
if let Ok(mut chain) = chain {
|
||||
result = if chain.is_valid() {
|
||||
IndicatorState::Success
|
||||
} else {
|
||||
IndicatorState::Error
|
||||
};
|
||||
|
||||
if let Some(ca_file) = &self.ca_file {
|
||||
if let Ok(ca_chain) = Chain::read(ca_file) {
|
||||
for ca_cert in ca_chain.into_vec() {
|
||||
chain.push(ca_cert);
|
||||
}
|
||||
result = if chain.is_valid() {
|
||||
IndicatorState::Success
|
||||
} else {
|
||||
IndicatorState::Error
|
||||
};
|
||||
} else {
|
||||
result = IndicatorState::Error;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key) = &self.key_file {
|
||||
match PrivateKey::read(Path::new(&key)) {
|
||||
Ok(private_key) => {
|
||||
if let Some(cert) = chain.certs().first() {
|
||||
return if cert.public_key_matches(private_key) && chain.is_valid() {
|
||||
result
|
||||
} else {
|
||||
IndicatorState::Error
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => return IndicatorState::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
PickCertFile,
|
||||
PickCaFile,
|
||||
PickKeyFile,
|
||||
ClearCertFile,
|
||||
ClearCaFile,
|
||||
ClearKeyFile,
|
||||
SetCertFile(Result<PathBuf, Error>),
|
||||
SetCaFile(Result<PathBuf, Error>),
|
||||
SetKeyFile(Result<PathBuf, Error>),
|
||||
Print,
|
||||
Merge,
|
||||
Copy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Error {
|
||||
Undefined,
|
||||
}
|
||||
|
||||
enum IndicatorState {
|
||||
Unknown,
|
||||
Success,
|
||||
Error,
|
||||
}
|
||||
|
||||
async fn pick_file() -> Result<PathBuf, Error> {
|
||||
let path = rfd::AsyncFileDialog::new()
|
||||
.set_title("Open file...")
|
||||
.pick_file()
|
||||
.await
|
||||
.ok_or(Error::Undefined)?;
|
||||
|
||||
Ok(path.into())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user