aboutsummaryrefslogblamecommitdiff
path: root/cmd/x509info/main.go
blob: c425c457a6ea6c7037ea46209f68172d24ed82a2 (plain) (tree)
1
2
3
4
5
6
7
8
9
10









                       

                                                 













                                                                                  





















                                                                                              

                                                            



































































































                                                                                                                                                                              
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)
	}
}