From 520e6fe582863247575c2a544ff92704fc234d14 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 21 Jan 2024 13:19:09 -0800 Subject: a tool to print information about a x509 certificate --- cmd/x509-info/README.md | 54 +++++++++++++++++ cmd/x509-info/main.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 cmd/x509-info/README.md create mode 100644 cmd/x509-info/main.go diff --git a/cmd/x509-info/README.md b/cmd/x509-info/README.md new file mode 100644 index 0000000..479771c --- /dev/null +++ b/cmd/x509-info/README.md @@ -0,0 +1,54 @@ +# x509-info + +At this point it's pretty clear that I'll never remember the syntax for `openssl` to show various information about a certificate. At last I will not have to google for that syntax ever again. + +## Usage + +``` shell +Usage: + x509-info [DOMAIN] + x509-info (-f long) [DOMAIN] + +Options: + -f, --format Format the result. Valid values: short, long. Default: short + -i, --insecure Skip the TLS validation. Default: false + -p, --port Specify the port. Default: 443 + -v, --version Print version information + -h, --help Print this message +``` + +The default format will print a short message: +``` shell +$ ./bin/x509-info github.com +github.com, valid until Thu, 14 Mar 2024 23:59:59 UTC (86 days left) +``` + +It's possible to get more details: +``` shell +$ ./bin/x509-info -f long github.com +certificate + version: 3 + serial: 17034156255497985825694118641198758684 + subject: github.com + issuer: DigiCert TLS Hybrid ECC SHA384 2020 CA1 + +validity: + not before: Tue, 14 Feb 2023 00:00:00 UTC + not after: Thu, 14 Mar 2024 23:59:59 UTC + validity days: 394 + remaining days: 86 + +SANs: + • github.com + • www.github.com +``` + +You can also check expired certificates: +``` shell +$ ./bin/x509-info -i expired.badssl.com +*.badssl.com, not valid since Sun, 12 Apr 2015 23:59:59 UTC (expired 3172 days ago) +``` + +## Notes + +Could the same be achieved with a wrapper around `openssl` ? yes. diff --git a/cmd/x509-info/main.go b/cmd/x509-info/main.go new file mode 100644 index 0000000..65ac548 --- /dev/null +++ b/cmd/x509-info/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "html/template" + "os" + "time" +) + +const usage = `Usage: + x509-info [DOMAIN] + x509-info (-f long) [DOMAIN] + +Options: + -f, --format Format the result. Valid values: short, long. Default: short + -i, --insecure Skip the TLS validation. Default: false + -p, --port Specify the port. Default: 443 + -v, --version Print version information + -h, --help Print this message +` + +var Version, BuildDate string + +func main() { + flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } + + var ( + portFlag int + outputFormatFlag string + insecureFlag bool + versionFlag bool + ) + + flag.IntVar(&portFlag, "port", 443, "Port to check") + flag.IntVar(&portFlag, "p", 443, "Port to check") + flag.StringVar(&outputFormatFlag, "format", "short", "Format the output") + flag.StringVar(&outputFormatFlag, "f", "short", "Format the output") + flag.BoolVar(&insecureFlag, "insecure", false, "Whether to bypass secure flag checks") + flag.BoolVar(&insecureFlag, "i", false, "Whether to bypass secure flag checks") + flag.BoolVar(&versionFlag, "version", false, "Print version information") + flag.BoolVar(&versionFlag, "v", false, "Print version information") + + flag.Parse() + + if versionFlag { + if Version != "" { + fmt.Printf("version: %s, build on: %s\n", Version, BuildDate) + return + } + fmt.Println("(unknown)") + return + } + + if flag.NArg() != 1 { + fmt.Fprintf(os.Stderr, "too many arguments: got %d, expected 1\n", flag.NArg()) + flag.Usage() + os.Exit(1) + } + + domain := flag.Arg(0) + + certs, err := getCertificates(domain, portFlag, insecureFlag) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + switch outputFormatFlag { + case "long": + printLong(certs) + default: + printShort(certs) + } +} + +func getCertificates(domain string, port int, insecureSkipVerify bool) ([]*x509.Certificate, error) { + conf := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + + remote := fmt.Sprintf("%s:%d", domain, port) + + conn, err := tls.Dial("tcp", remote, conf) + if err != nil { + return nil, fmt.Errorf("failed to get the certificate for %s: %v", remote, err) + } + + defer conn.Close() + + certs := conn.ConnectionState().PeerCertificates + return certs, nil +} + +func printShort(certs []*x509.Certificate) { + cert := certs[0] + + now := time.Now() + remainingDays := cert.NotAfter.Sub(now) + + if remainingDays > 0 { + fmt.Printf("%s, valid until %s (%d days left)\n", cert.Subject.CommonName, cert.NotAfter.Format(time.RFC1123), int(remainingDays.Hours()/24)) + } else { + fmt.Printf("%s, not valid since %s (expired %d days ago)\n", cert.Subject.CommonName, cert.NotAfter.Format(time.RFC1123), int(remainingDays.Abs().Hours()/24)) + } +} + +const tmplLong = `certificate + version: {{ .Version }} + serial: {{ .SerialNumber }} + subject: {{ .Subject.CommonName }} + issuer: {{ .Issuer.CommonName }} + +validity: + not before: {{ rfc1123 .NotBefore }} + not after: {{ rfc1123 .NotAfter }} + validity days: {{ validFor .NotBefore .NotAfter }} + remaining days: {{ remainingDays .NotAfter }} + +SANs: +{{- range $i, $name := .DNSNames }} + • {{ $name }} +{{- end }} +` + +func printLong(certs []*x509.Certificate) { + funcMap := template.FuncMap{ + "validFor": func(before, after time.Time) int { + validForDays := after.Sub(before) + return int(validForDays.Hours() / 24) + }, + "remainingDays": func(notAfter time.Time) int { + now := time.Now() + remainingDays := notAfter.Sub(now) + return int(remainingDays.Hours() / 24) + }, + "rfc1123": func(date time.Time) string { + return date.Format(time.RFC1123) + }, + } + + tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(tmplLong) + if err != nil { + panic(err) + } + + err = tmpl.Execute(os.Stdout, certs[0]) + if err != nil { + panic(err) + } +} -- cgit 1.4.1