mirror of
https://github.com/pcvolkmer/cert-tools.git
synced 2025-04-19 09:06:49 +00:00
feat: show information about the loaded chain
This commit is contained in:
parent
3ec36c1795
commit
13146f474b
@ -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`.
|
||||||
|
|
||||||
|

|
BIN
ui-image.jpeg
Normal file
BIN
ui-image.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
@ -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"
|
259
ui/src/main.rs
259
ui/src/main.rs
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,53 +290,174 @@ impl Ui {
|
|||||||
|
|
||||||
let certs = {
|
let certs = {
|
||||||
let mut result = column![];
|
let mut result = column![];
|
||||||
|
|
||||||
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![
|
row![text("Issuer: ").width(200), text(cert.issuer().to_string())],
|
||||||
text("Name: ").width(200),
|
row![
|
||||||
text(cert.name().to_string())
|
text("Gültigkeit: ").width(200),
|
||||||
],
|
text("Gültig von "),
|
||||||
row![
|
if cert.is_valid_not_before(&SystemTime::now()) {
|
||||||
text("Issuer: ").width(200),
|
text(cert.not_before().to_string())
|
||||||
text(cert.issuer().to_string())
|
} else {
|
||||||
],
|
text(cert.not_before().to_string())
|
||||||
row![
|
.color(Color::parse("#aa0000").unwrap())
|
||||||
text("SHA-1-Fingerprint: ").width(200),
|
},
|
||||||
text(cert.fingerprint().sha1.to_string())
|
text(" bis "),
|
||||||
],
|
if cert.is_valid_not_after(&SystemTime::now()) {
|
||||||
row![
|
text(cert.not_after().to_string())
|
||||||
text("SHA-256-Fingerprint: ").width(200),
|
} else {
|
||||||
text(cert.fingerprint().sha256.to_string())
|
text(cert.not_after().to_string())
|
||||||
],
|
.color(Color::parse("#aa0000").unwrap())
|
||||||
row![
|
}
|
||||||
text("Subject-Key-Id: ").width(200),
|
],
|
||||||
text(cert.subject_key_id().to_string())
|
row![
|
||||||
],
|
text("SHA-1-Fingerprint: ").width(200),
|
||||||
row![
|
text(cert.fingerprint().sha1.to_string())
|
||||||
text("Authority_Key-Id: ").width(200),
|
],
|
||||||
text(cert.authority_key_id().to_string())
|
row![
|
||||||
],
|
text("SHA-256-Fingerprint: ").width(200),
|
||||||
])
|
text(cert.fingerprint().sha256.to_string())
|
||||||
.padding(4)
|
],
|
||||||
.style(|t| container::Style {
|
row![
|
||||||
border: Border::default().width(1),
|
text("Subject-Key-Id: ").width(200),
|
||||||
background: Some(Background::Color(Color::parse("#eee").unwrap())),
|
text(cert.subject_key_id().to_string())
|
||||||
..container::Style::default()
|
],
|
||||||
})
|
row![
|
||||||
.width(Length::Fill),
|
text("Authority_Key-Id: ").width(200),
|
||||||
)
|
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(8)
|
||||||
|
.style(|_| container::Style {
|
||||||
|
background: Some(Background::Color(Color::WHITE)),
|
||||||
|
..container::Style::default()
|
||||||
|
})
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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)
|
||||||
@ -497,7 +624,7 @@ Authority-Key-Id: {}
|
|||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_chain(&self) -> Result<Chain, String> {
|
fn load_chain(&self) -> Result<Chain, String> {
|
||||||
if let Some(cert_file) = &self.cert_file {
|
if let Some(cert_file) = &self.cert_file {
|
||||||
let chain = Chain::read(cert_file);
|
let chain = Chain::read(cert_file);
|
||||||
@ -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)]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user