1
0
mirror of https://github.com/pcvolkmer/cert-tools.git synced 2025-04-19 17:06:49 +00:00

feat: show information about the loaded chain

This commit is contained in:
Paul-Christian Volkmer 2025-01-06 11:41:38 +01:00
parent 3ec36c1795
commit 13146f474b
4 changed files with 198 additions and 68 deletions

View File

@ -60,4 +60,7 @@ cert-tools merge cert.pem ca.pem > chain.pem
## GUI ## GUI
In addition to the console-based application, a simple graphical user interface is available in (sub-)package `ui`. In addition to the console-based application, a simple [iced](https://github.com/iced-rs/iced)-based graphical user
interface is available in (sub-)package `ui`.
![](ui-image.jpeg)

BIN
ui-image.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -5,6 +5,6 @@ edition = "2021"
[dependencies] [dependencies]
cert-tools = { path = "..", version = "*" } cert-tools = { path = "..", version = "*" }
iced = { version = "0.13", features = ["tiny-skia"], default-features = false } iced = { version = "0.13", features = ["wgpu", "tiny-skia", "tokio"], default-features = false }
rfd = "0.15" rfd = "0.15"
itertools = "0.14" itertools = "0.14"

View File

@ -6,12 +6,11 @@ use iced::widget::{
button, column, container, horizontal_rule, horizontal_space, row, text, text_editor, button, column, container, horizontal_rule, horizontal_space, row, text, text_editor,
Container, Scrollable, Container, Scrollable,
}; };
use iced::{ use iced::{application, clipboard, Background, Color, Element, Font, Length, Size, Task};
application, clipboard, Background, Border, Color, Element, Font, Length, Shadow, Size, Task,
};
use itertools::Itertools; use itertools::Itertools;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime;
fn main() -> iced::Result { fn main() -> iced::Result {
application(Ui::title, Ui::update, Ui::view) application(Ui::title, Ui::update, Ui::view)
@ -23,7 +22,7 @@ fn main() -> iced::Result {
enum UiMode { enum UiMode {
CertList, CertList,
Output Output,
} }
struct Ui { struct Ui {
@ -44,7 +43,7 @@ impl Ui {
cert_file: None, cert_file: None,
ca_file: None, ca_file: None,
key_file: None, key_file: None,
mode: UiMode::Output, mode: UiMode::CertList,
chain: None, chain: None,
output: Content::default(), output: Content::default(),
status: String::new(), status: String::new(),
@ -58,27 +57,34 @@ impl Ui {
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
self.mode = UiMode::Output; self.mode = UiMode::CertList;
match message { match message {
Message::PickCertFile => Task::perform(pick_file(), Message::SetCertFile), Message::PickCertFile => Task::perform(pick_file(), Message::SetCertFile),
Message::PickCaFile => Task::perform(pick_file(), Message::SetCaFile), Message::PickCaFile => Task::perform(pick_file(), Message::SetCaFile),
Message::PickKeyFile => Task::perform(pick_file(), Message::SetKeyFile), Message::PickKeyFile => Task::perform(pick_file(), Message::SetKeyFile),
Message::ClearCertFile => { Message::ClearCertFile => {
self.cert_file = None; self.cert_file = None;
self.chain = None; self.chain = match self.load_chain() {
self.output = Content::default(); Ok(chain) => Some(chain),
Task::none() _ => None,
};
self.update(Message::Print)
} }
Message::ClearCaFile => { Message::ClearCaFile => {
self.ca_file = None; self.ca_file = None;
self.chain = None; self.chain = match self.load_chain() {
self.output = Content::default(); Ok(chain) => Some(chain),
Task::none() _ => None,
};
self.update(Message::Print)
} }
Message::ClearKeyFile => { Message::ClearKeyFile => {
self.key_file = None; self.key_file = None;
self.output = Content::default(); self.chain = match self.load_chain() {
Task::none() Ok(chain) => Some(chain),
_ => None,
};
self.update(Message::Print)
} }
Message::SetCertFile(file) => { Message::SetCertFile(file) => {
match file { match file {
@ -86,14 +92,14 @@ impl Ui {
self.cert_file = Some(file); self.cert_file = Some(file);
self.chain = match self.load_chain() { self.chain = match self.load_chain() {
Ok(chain) => Some(chain), Ok(chain) => Some(chain),
_ => None _ => None,
}; };
self.output = Content::default(); self.output = Content::default();
self.mode = UiMode::CertList; self.mode = UiMode::CertList;
} }
_ => self.cert_file = None, _ => self.cert_file = None,
}; };
Task::none() self.update(Message::Print)
} }
Message::SetCaFile(file) => { Message::SetCaFile(file) => {
match file { match file {
@ -101,25 +107,23 @@ impl Ui {
self.ca_file = Some(file); self.ca_file = Some(file);
self.chain = match self.load_chain() { self.chain = match self.load_chain() {
Ok(chain) => Some(chain), Ok(chain) => Some(chain),
_ => None _ => None,
}; };
self.output = Content::default(); self.output = Content::default();
self.mode = UiMode::CertList;
} }
_ => self.ca_file = None, _ => self.ca_file = None,
}; };
Task::none() self.update(Message::Print)
} }
Message::SetKeyFile(file) => { Message::SetKeyFile(file) => {
match file { match file {
Ok(file) => { Ok(file) => {
self.key_file = Some(file); self.key_file = Some(file);
self.output = Content::default(); self.output = Content::default();
self.mode = UiMode::CertList;
} }
_ => self.key_file = None, _ => self.key_file = None,
}; };
Task::none() self.update(Message::Print)
} }
Message::Print => { Message::Print => {
match self.print_output() { match self.print_output() {
@ -140,6 +144,7 @@ impl Ui {
Ok(output) => { Ok(output) => {
self.output = Content::with_text(output.as_str()); self.output = Content::with_text(output.as_str());
self.status = String::new(); self.status = String::new();
self.mode = UiMode::Output;
} }
Err(err) => { Err(err) => {
self.output = Content::default(); self.output = Content::default();
@ -148,7 +153,7 @@ impl Ui {
}; };
Task::none() Task::none()
} }
Message::Copy => clipboard::write::<Message>(self.output.text()) Message::Copy => clipboard::write::<Message>(self.output.text()),
} }
} }
@ -288,17 +293,27 @@ impl Ui {
if let Some(chain) = &self.chain { if let Some(chain) = &self.chain {
for cert in chain.certs() { for cert in chain.certs() {
result = result = result.push(
result.push(
Container::new(column![ Container::new(column![
text(cert.name().to_string()).size(18), text(cert.name().to_string()).size(18),
horizontal_rule(1),
row![text("Issuer: ").width(200), text(cert.issuer().to_string())],
row![ row![
text("Name: ").width(200), text("Gültigkeit: ").width(200),
text(cert.name().to_string()) text("Gültig von "),
], if cert.is_valid_not_before(&SystemTime::now()) {
row![ text(cert.not_before().to_string())
text("Issuer: ").width(200), } else {
text(cert.issuer().to_string()) text(cert.not_before().to_string())
.color(Color::parse("#aa0000").unwrap())
},
text(" bis "),
if cert.is_valid_not_after(&SystemTime::now()) {
text(cert.not_after().to_string())
} else {
text(cert.not_after().to_string())
.color(Color::parse("#aa0000").unwrap())
}
], ],
row![ row![
text("SHA-1-Fingerprint: ").width(200), text("SHA-1-Fingerprint: ").width(200),
@ -316,11 +331,18 @@ impl Ui {
text("Authority_Key-Id: ").width(200), text("Authority_Key-Id: ").width(200),
text(cert.authority_key_id().to_string()) text(cert.authority_key_id().to_string())
], ],
if cert.dns_names().is_empty() {
row![]
} else {
row![
text("DNS-Names: ").width(200),
text(cert.dns_names().join(", "))
]
},
]) ])
.padding(4) .padding(8)
.style(|t| container::Style { .style(|_| container::Style {
border: Border::default().width(1), background: Some(Background::Color(Color::WHITE)),
background: Some(Background::Color(Color::parse("#eee").unwrap())),
..container::Style::default() ..container::Style::default()
}) })
.width(Length::Fill), .width(Length::Fill),
@ -328,10 +350,114 @@ impl Ui {
} }
}; };
let content = result.spacing(2); let content =
Container::new(result.spacing(4))
.padding(4)
.style(|_| container::Style {
background: Some(Background::Color(Color::parse("#eeeeee").unwrap())),
..container::Style::default()
});
Scrollable::new(content).height(Length::Fill) Scrollable::new(content).height(Length::Fill)
}; };
let chain_info = {
let mut result = column![];
result = result.push(if let Some(chain) = &self.chain {
if chain.has_missing_tail() {
column![
Container::new(text("Last Certificate points to another one that should be contained in chain.")).style(|_| container::Style {
background: Some(Background::Color(Color::parse("#eeaa00").unwrap())),
text_color: Some(Color::WHITE),
..container::Style::default()
}).padding(2).width(Length::Fill),
Container::new(text("Self signed (CA-) Certificate? It might be required to import a self signed Root-CA manually for applications to use it."))
.padding(2)
]
} else {
column![]
}
} else {
column![]
});
result = result.push(if let Some(chain) = &self.chain {
if chain.is_valid() {
column![Container::new(text("Chain is valid"))
.style(|_| container::Style {
background: Some(Background::Color(Color::parse("#00aa00").unwrap())),
text_color: Some(Color::WHITE),
..container::Style::default()
})
.padding(2)
.width(Length::Fill)]
} else if !chain.certs().is_empty() {
column![Container::new(text(
"Chain or some of its parts is not valid (anymore)"
))
.style(|_| container::Style {
background: Some(Background::Color(Color::parse("#aa0000").unwrap())),
text_color: Some(Color::WHITE),
..container::Style::default()
})
.padding(2)
.width(Length::Fill)]
} else {
column![]
}
} else {
column![]
});
result =
result.push(if let Some(key) = &self.key_file {
match PrivateKey::read(Path::new(&key)) {
Ok(private_key) => {
if let Some(chain) = &self.chain {
if let Some(first) = chain.certs().first() {
if first.public_key_matches(private_key) {
column![Container::new(text(
"Private Key matches first Cert Public Key"
))
.style(|_| container::Style {
background: Some(Background::Color(
Color::parse("#00aa00").unwrap()
)),
text_color: Some(Color::WHITE),
..container::Style::default()
})
.padding(2)
.width(Length::Fill)]
} else {
column![Container::new(text(
"Private Key does not match the first Cert Public Key"
))
.style(|_| container::Style {
background: Some(Background::Color(
Color::parse("#aa0000").unwrap()
)),
text_color: Some(Color::WHITE),
..container::Style::default()
})
.padding(2)
.width(Length::Fill)]
}
} else {
column![]
}
} else {
column![]
}
}
_ => column![],
}
} else {
column![]
});
result
};
let indicator = { let indicator = {
let content = match self.indicator_state() { let content = match self.indicator_state() {
IndicatorState::Unknown => ("?", "#aaaaaa", "#ffffff"), IndicatorState::Unknown => ("?", "#aaaaaa", "#ffffff"),
@ -361,9 +487,10 @@ impl Ui {
.spacing(96), .spacing(96),
horizontal_rule(1), horizontal_rule(1),
buttons, buttons,
horizontal_rule(1),
match self.mode { match self.mode {
UiMode::CertList => certs, UiMode::CertList => column![certs, chain_info],
UiMode::Output => output UiMode::Output => column![output],
}, },
horizontal_rule(1), horizontal_rule(1),
text(&self.status) text(&self.status)
@ -582,7 +709,7 @@ enum Message {
SetKeyFile(Result<PathBuf, Error>), SetKeyFile(Result<PathBuf, Error>),
Print, Print,
Merge, Merge,
Copy Copy,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]