aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ssh-cert-info/Cargo.toml6
-rw-r--r--src/ssh-cert-info/README.org1
-rw-r--r--src/ssh-cert-info/src/main.rs225
3 files changed, 232 insertions, 0 deletions
diff --git a/src/ssh-cert-info/Cargo.toml b/src/ssh-cert-info/Cargo.toml
new file mode 100644
index 0000000..f00f2c5
--- /dev/null
+++ b/src/ssh-cert-info/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "ssh-cert-info"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
diff --git a/src/ssh-cert-info/README.org b/src/ssh-cert-info/README.org
new file mode 100644
index 0000000..9d10f5f
--- /dev/null
+++ b/src/ssh-cert-info/README.org
@@ -0,0 +1 @@
+A quick way to check information about various SSH certificates on my machine.
diff --git a/src/ssh-cert-info/src/main.rs b/src/ssh-cert-info/src/main.rs
new file mode 100644
index 0000000..4c0c1ff
--- /dev/null
+++ b/src/ssh-cert-info/src/main.rs
@@ -0,0 +1,225 @@
+use std::env;
+use std::fs;
+use std::path::PathBuf;
+use std::process::Command;
+
+fn main() {
+ let ssh_dir = get_ssh_directory();
+
+ match fs::read_dir(&ssh_dir) {
+ Ok(entries) => {
+ let mut cert_files = Vec::new();
+
+ for entry in entries {
+ if let Ok(entry) = entry {
+ let path = entry.path();
+ if let Some(extension) = path.extension() {
+ if extension == "pub" {
+ // I use the following convention for my certificates: <identificator>-cert.pub
+ if let Some(filename) = path.file_name() {
+ if filename.to_string_lossy().contains("-cert.pub") {
+ cert_files.push(path);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if cert_files.is_empty() {
+ println!("No SSH certificates found in {}", ssh_dir.display());
+ return;
+ }
+
+ println!("SSH Certificate Report");
+ println!("======================");
+ println!();
+
+ for cert_path in cert_files {
+ check_certificate(&cert_path);
+ println!();
+ }
+ }
+ Err(e) => {
+ eprintln!("Error reading SSH directory {}: {}", ssh_dir.display(), e);
+ std::process::exit(1);
+ }
+ }
+}
+
+fn get_ssh_directory() -> PathBuf {
+ let home = env::var("HOME").unwrap_or_else(|_| {
+ eprintln!("HOME environment variable not set");
+ std::process::exit(1);
+ });
+
+ PathBuf::from(home).join(".ssh")
+}
+
+fn check_certificate(cert_path: &PathBuf) {
+ println!("Certificate: {}", cert_path.display());
+
+ // Use ssh-keygen to get certificate information
+ // TODO: maybe consider https://docs.rs/ssh-key/latest/ssh_key/certificate/struct.Certificate.html ?
+ let output = Command::new("ssh-keygen")
+ .arg("-L")
+ .arg("-f")
+ .arg(cert_path)
+ .output();
+
+ match output {
+ Ok(output) => {
+ if output.status.success() {
+ let cert_info = String::from_utf8_lossy(&output.stdout);
+ parse_and_display_cert_info(&cert_info);
+ } else {
+ let error = String::from_utf8_lossy(&output.stderr);
+ println!(" Error: Failed to read certificate: {}", error.trim());
+ }
+ }
+ Err(e) => {
+ println!(" Error: Failed to execute ssh-keygen: {}", e);
+ }
+ }
+}
+
+fn parse_and_display_cert_info(cert_info: &str) {
+ let mut type_found = false;
+ let mut public_key = String::new();
+ let mut signing_ca = String::new();
+ let mut key_id = String::new();
+ let mut serial = String::new();
+ let mut valid_from = String::new();
+ let mut valid_to = String::new();
+ let mut cert_type = String::new();
+ let mut principals = Vec::new();
+ let mut critical_options = Vec::new();
+ let mut extensions = Vec::new();
+
+ for line in cert_info.lines() {
+ let line = line.trim();
+
+ if line.starts_with("Type:") {
+ cert_type = line.replace("Type:", "").trim().to_string();
+ type_found = true;
+ } else if line.starts_with("Public key:") {
+ public_key = line.replace("Public key:", "").trim().to_string();
+ } else if line.starts_with("Signing CA:") {
+ signing_ca = line.replace("Signing CA:", "").trim().to_string();
+ } else if line.starts_with("Key ID:") {
+ key_id = line.replace("Key ID:", "").trim().to_string();
+ } else if line.starts_with("Serial:") {
+ serial = line.replace("Serial:", "").trim().to_string();
+ } else if line.starts_with("Valid: from") {
+ // Format: "Valid: from 2023-01-01T00:00:00 to 2024-01-01T00:00:00"
+ let valid_line = line.replace("Valid: from", "").trim().to_string();
+ if let Some(to_pos) = valid_line.find(" to ") {
+ valid_from = valid_line[..to_pos].trim().to_string();
+ valid_to = valid_line[to_pos + 4..].trim().to_string();
+ }
+ } else if line.starts_with("Principals:") {
+ // Principals might be on the same line or following lines
+ let principal_text = line.replace("Principals:", "").trim().to_string();
+ if !principal_text.is_empty() {
+ principals.push(principal_text);
+ }
+ } else if line.starts_with("Critical Options:") {
+ let options_text = line.replace("Critical Options:", "").trim().to_string();
+ if !options_text.is_empty() {
+ critical_options.push(options_text);
+ }
+ } else if line.starts_with("Extensions:") {
+ let extensions_text = line.replace("Extensions:", "").trim().to_string();
+ if !extensions_text.is_empty() {
+ extensions.push(extensions_text);
+ }
+ } else if !type_found {
+ // Skip lines before we find the certificate type
+ continue;
+ } else if line.len() > 0 && (line.starts_with(" ") || line.starts_with("\t\t")) {
+ // This might be a continuation of principals, critical options, or extensions
+ let trimmed = line.trim();
+ if !trimmed.is_empty() {
+ if !principals.is_empty() {
+ principals.push(trimmed.to_string());
+ } else if !critical_options.is_empty() {
+ critical_options.push(trimmed.to_string());
+ } else if !extensions.is_empty() {
+ extensions.push(trimmed.to_string());
+ }
+ }
+ }
+ }
+
+ if !cert_type.is_empty() {
+ println!(" Type: {}", cert_type);
+ }
+
+ if !public_key.is_empty() {
+ println!(" Public key: {}", public_key);
+ }
+
+ if !signing_ca.is_empty() {
+ println!(" Signing CA: {}", signing_ca);
+ }
+
+ if !key_id.is_empty() {
+ println!(" Key ID: {}", key_id);
+ }
+
+ if !serial.is_empty() {
+ println!(" Serial: {}", serial);
+ }
+
+ if !valid_from.is_empty() && !valid_to.is_empty() {
+ println!(" Valid from: {}", valid_from);
+ println!(" Valid to: {}", valid_to);
+
+ check_expiration(&valid_to);
+ }
+
+ if !principals.is_empty() {
+ println!(" Principals:");
+ for principal in &principals {
+ if !principal.is_empty() {
+ println!(" {}", principal);
+ }
+ }
+ }
+
+ if !critical_options.is_empty() {
+ println!(" Critical Options:");
+ for option in &critical_options {
+ if !option.is_empty() {
+ println!(" {}", option);
+ }
+ }
+ }
+
+ if !extensions.is_empty() {
+ println!(" Extensions:");
+ for extension in &extensions {
+ if !extension.is_empty() {
+ println!(" {}", extension);
+ }
+ }
+ }
+}
+
+fn check_expiration(valid_to: &str) {
+ // TODO: maybe use chrono for more robust date handling ?
+ let current_date = std::process::Command::new("date")
+ .arg("+%Y-%m-%dT%H:%M:%S")
+ .output();
+
+ if let Ok(output) = current_date {
+ if let Ok(current) = String::from_utf8(output.stdout) {
+ let current = current.trim();
+ if valid_to < current {
+ println!(" ⚠️ WARNING: Certificate has EXPIRED!");
+ } else {
+ println!(" ✓ Certificate is currently valid");
+ }
+ }
+ }
+}