aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml2
-rw-r--r--cmd/ssh-cert-info/README.org (renamed from src/ssh-cert-info/README.org)0
-rw-r--r--cmd/ssh-cert-info/main.go162
-rw-r--r--go.mod3
-rw-r--r--go.sum4
-rw-r--r--src/ssh-cert-info/Cargo.toml6
-rw-r--r--src/ssh-cert-info/src/main.rs225
7 files changed, 169 insertions, 233 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 69724a1..c1ce08a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,3 @@
[workspace]
-members = ["src/apple_silicon/", "src/ssh-cert-info", "src/x509-info/"]
+members = ["src/apple_silicon/", "src/x509-info/"]
resolver = "3"
diff --git a/src/ssh-cert-info/README.org b/cmd/ssh-cert-info/README.org
index 9d10f5f..9d10f5f 100644
--- a/src/ssh-cert-info/README.org
+++ b/cmd/ssh-cert-info/README.org
diff --git a/cmd/ssh-cert-info/main.go b/cmd/ssh-cert-info/main.go
new file mode 100644
index 0000000..957dd93
--- /dev/null
+++ b/cmd/ssh-cert-info/main.go
@@ -0,0 +1,162 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/ssh"
+)
+
+func main() {
+ sshDir, err := getSSHDirectory()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error getting SSH directory: %v\n", err)
+ os.Exit(1)
+ }
+
+ certFiles, err := findCertificateFiles(sshDir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error reading SSH directory %s: %v\n", sshDir, err)
+ os.Exit(1)
+ }
+
+ if len(certFiles) == 0 {
+ fmt.Printf("No SSH certificates found in %s\n", sshDir)
+ return
+ }
+
+ fmt.Println("SSH Certificate Report")
+ fmt.Println("======================")
+ fmt.Println()
+
+ for _, certPath := range certFiles {
+ checkCertificate(certPath)
+ fmt.Println()
+ }
+}
+
+func getSSHDirectory() (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("failed to get home directory: %w", err)
+ }
+ return filepath.Join(home, ".ssh"), nil
+}
+
+func findCertificateFiles(sshDir string) ([]string, error) {
+ entries, err := os.ReadDir(sshDir)
+ if err != nil {
+ return nil, err
+ }
+
+ var certFiles []string
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+
+ name := entry.Name()
+ if strings.HasSuffix(name, ".pub") && strings.Contains(name, "-cert.pub") {
+ certFiles = append(certFiles, filepath.Join(sshDir, name))
+ }
+ }
+
+ return certFiles, nil
+}
+
+func checkCertificate(certPath string) {
+ fmt.Printf("Certificate: %s\n", certPath)
+
+ certData, err := os.ReadFile(certPath)
+ if err != nil {
+ fmt.Printf(" Error: Failed to read certificate file: %v\n", err)
+ return
+ }
+
+ pubKey, _, _, _, err := ssh.ParseAuthorizedKey(certData)
+ if err != nil {
+ fmt.Printf(" Error: Failed to parse certificate: %v\n", err)
+ return
+ }
+
+ cert, ok := pubKey.(*ssh.Certificate)
+ if !ok {
+ fmt.Printf(" Error: Not an SSH certificate\n")
+ return
+ }
+
+ displayCertificateInfo(cert)
+}
+
+func displayCertificateInfo(cert *ssh.Certificate) {
+ certType := "Unknown"
+ switch cert.CertType {
+ case ssh.UserCert:
+ certType = "ssh-ed25519-cert-v01@openssh.com user certificate"
+ case ssh.HostCert:
+ certType = "ssh-ed25519-cert-v01@openssh.com host certificate"
+ }
+ fmt.Printf(" Type: %s\n", certType)
+
+ fmt.Printf(" Public key: %s %s\n", cert.Key.Type(), cert.KeyId)
+
+ fmt.Printf(" Signing CA: %s\n", cert.SignatureKey.Type())
+
+ fmt.Printf(" Key ID: %s\n", cert.KeyId)
+
+ fmt.Printf(" Serial: %d\n", cert.Serial)
+
+ validFrom := time.Unix(int64(cert.ValidAfter), 0)
+ validTo := time.Unix(int64(cert.ValidBefore), 0)
+
+ fmt.Printf(" Valid from: %s\n", validFrom.Format("2006-01-02T15:04:05"))
+
+ if cert.ValidBefore == ssh.CertTimeInfinity {
+ fmt.Printf(" Valid to: forever\n")
+ fmt.Println(" ✓ Certificate is currently valid")
+ } else {
+ fmt.Printf(" Valid to: %s\n", validTo.Format("2006-01-02T15:04:05"))
+ checkCertificateExpiration(validTo)
+ }
+
+ if len(cert.ValidPrincipals) > 0 {
+ fmt.Println(" Principals:")
+ for _, principal := range cert.ValidPrincipals {
+ fmt.Printf(" %s\n", principal)
+ }
+ }
+
+ if len(cert.CriticalOptions) > 0 {
+ fmt.Println(" Critical Options:")
+ for key, value := range cert.CriticalOptions {
+ if value == "" {
+ fmt.Printf(" %s\n", key)
+ } else {
+ fmt.Printf(" %s %s\n", key, value)
+ }
+ }
+ }
+
+ if len(cert.Extensions) > 0 {
+ fmt.Println(" Extensions:")
+ for key, value := range cert.Extensions {
+ if value == "" {
+ fmt.Printf(" %s\n", key)
+ } else {
+ fmt.Printf(" %s %s\n", key, value)
+ }
+ }
+ }
+}
+
+func checkCertificateExpiration(validTo time.Time) {
+ now := time.Now()
+ if validTo.Before(now) {
+ fmt.Println(" ⚠️ WARNING: Certificate has EXPIRED!")
+ } else {
+ fmt.Println(" ✓ Certificate is currently valid")
+ }
+}
diff --git a/go.mod b/go.mod
index 702811b..71f1388 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
- golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/crypto v0.41.0 // indirect
+ golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
diff --git a/go.sum b/go.sum
index a9149e9..812b501 100644
--- a/go.sum
+++ b/go.sum
@@ -32,10 +32,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
diff --git a/src/ssh-cert-info/Cargo.toml b/src/ssh-cert-info/Cargo.toml
deleted file mode 100644
index f00f2c5..0000000
--- a/src/ssh-cert-info/Cargo.toml
+++ /dev/null
@@ -1,6 +0,0 @@
-[package]
-name = "ssh-cert-info"
-version = "0.1.0"
-edition = "2024"
-
-[dependencies]
diff --git a/src/ssh-cert-info/src/main.rs b/src/ssh-cert-info/src/main.rs
deleted file mode 100644
index 4c0c1ff..0000000
--- a/src/ssh-cert-info/src/main.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-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");
- }
- }
- }
-}