diff options
Diffstat (limited to '')
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -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.go | 162 | ||||
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | src/ssh-cert-info/Cargo.toml | 6 | ||||
| -rw-r--r-- | src/ssh-cert-info/src/main.rs | 225 |
7 files changed, 169 insertions, 233 deletions
@@ -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") + } +} @@ -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 ) @@ -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"); - } - } - } -} |
