package main import ( "crypto/tls" "crypto/x509" "flag" "fmt" "os" "time" ) type OutputFormat string const ( FormatShort OutputFormat = "short" FormatLong OutputFormat = "long" ) type Config struct { Domain string Port int Insecure bool Format OutputFormat } func main() { var config Config var formatStr string flag.StringVar(&config.Domain, "domain", "", "Domain to check (required)") flag.IntVar(&config.Port, "port", 443, "Port to check") flag.BoolVar(&config.Insecure, "insecure", false, "Accept invalid certificate") flag.StringVar(&formatStr, "format", "short", "Output format (short|long)") flag.Parse() if config.Domain == "" { if len(flag.Args()) == 0 { fmt.Fprintf(os.Stderr, "Error: domain is required\n") flag.Usage() os.Exit(1) } config.Domain = flag.Args()[0] } switch formatStr { case "short": config.Format = FormatShort case "long": config.Format = FormatLong default: fmt.Fprintf(os.Stderr, "Error: invalid format '%s', must be 'short' or 'long'\n", formatStr) os.Exit(1) } cert, err := getCertificate(config.Domain, config.Port, config.Insecure) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } printCertificate(cert, config.Format) } func getCertificate(domain string, port int, insecure bool) (*x509.Certificate, error) { address := fmt.Sprintf("%s:%d", domain, port) tlsConfig := &tls.Config{ ServerName: domain, InsecureSkipVerify: insecure, } conn, err := tls.Dial("tcp", address, tlsConfig) if err != nil { return nil, fmt.Errorf("failed to connect to %s: %w", address, err) } defer func() { if closeErr := conn.Close(); closeErr != nil { // Log the error but don't override the main function's return value fmt.Fprintf(os.Stderr, "warning: failed to close connection: %v\n", closeErr) } }() certs := conn.ConnectionState().PeerCertificates if len(certs) == 0 { return nil, fmt.Errorf("no certificate found for %s", domain) } return certs[0], nil } func printCertificate(cert *x509.Certificate, format OutputFormat) { switch format { case FormatShort: printShort(cert) case FormatLong: printLong(cert) } } func printShort(cert *x509.Certificate) { remaining := time.Until(cert.NotAfter) commonName := getCommonName(cert) if remaining >= 0 { days := int(remaining.Hours() / 24) fmt.Printf("%s: %s (%d days left)\n", commonName, cert.NotAfter.Format(time.RFC1123Z), days) } else { days := int(-remaining.Hours() / 24) fmt.Printf("%s: %s (it expired %d days ago)\n", commonName, cert.NotAfter.Format(time.RFC1123Z), days) } } func printLong(cert *x509.Certificate) { remaining := time.Until(cert.NotAfter) validityDuration := cert.NotAfter.Sub(cert.NotBefore) fmt.Println("certificate") fmt.Printf(" version: %d\n", cert.Version) fmt.Printf(" serial: %s\n", cert.SerialNumber.String()) fmt.Printf(" subject: %s\n", cert.Subject.String()) fmt.Printf(" issuer: %s\n", cert.Issuer.String()) fmt.Println(" validity") fmt.Printf(" not before : %s\n", cert.NotBefore.Format(time.RFC1123Z)) fmt.Printf(" not after : %s\n", cert.NotAfter.Format(time.RFC1123Z)) fmt.Printf(" validity days : %d\n", int(validityDuration.Hours()/24)) fmt.Printf(" remaining days: %d\n", int(remaining.Hours()/24)) fmt.Println(" SANs:") printSANs(cert) } func getCommonName(cert *x509.Certificate) string { if cert.Subject.CommonName != "" { return cert.Subject.CommonName } return "" } func printSANs(cert *x509.Certificate) { // DNS names for _, name := range cert.DNSNames { fmt.Printf(" DNS:%s\n", name) } // IP addresses for _, ip := range cert.IPAddresses { fmt.Printf(" IP address:%s\n", ip.String()) } // Email addresses for _, email := range cert.EmailAddresses { fmt.Printf(" Email:%s\n", email) } // URIs for _, uri := range cert.URIs { fmt.Printf(" URI:%s\n", uri.String()) } }