package main
import (
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"html/template"
"os"
"time"
"github.com/fcuny/world/internal/version"
)
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
`
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 {
information := version.VersionAndBuildInfo()
fmt.Println(information)
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)
}
}