about summary refs log tree commit diff
path: root/cmd/x509-info
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/x509-info')
-rw-r--r--cmd/x509-info/README.md54
-rw-r--r--cmd/x509-info/main.go153
2 files changed, 207 insertions, 0 deletions
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)
+	}
+}