cert-deets/main.go

323 lines
7.2 KiB
Go

package main
import (
"context"
"crypto/sha1"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/markkurossi/tabulate"
flag "github.com/spf13/pflag"
)
const sanGrouping = 4
type SAN interface {
string | net.IP
}
type args struct {
url string
address string
}
type chainIssue struct {
certIndex int
reason string
}
func diagnoseChain(certs []*x509.Certificate) *chainIssue {
now := time.Now()
for i, cert := range certs {
if now.Before(cert.NotBefore) {
return &chainIssue{i, fmt.Sprintf("not yet valid (valid from %s)", cert.NotBefore.Format("2006-01-02"))}
}
if now.After(cert.NotAfter) {
return &chainIssue{i, fmt.Sprintf("expired on %s", cert.NotAfter.Format("2006-01-02"))}
}
}
for i := 0; i < len(certs)-1; i++ {
if err := certs[i].CheckSignatureFrom(certs[i+1]); err != nil {
return &chainIssue{i, fmt.Sprintf("signature not valid (not signed by %s)", certs[i+1].Subject.CommonName)}
}
}
roots, err := x509.SystemCertPool()
if err == nil {
last := certs[len(certs)-1]
_, err := last.Verify(x509.VerifyOptions{Roots: roots})
if err != nil {
return &chainIssue{len(certs) - 1, "root not trusted by system"}
}
}
return nil
}
func fmtChaincert(cert *x509.Certificate, issue *chainIssue, idx int) string {
var caChain []string
label := fmt.Sprintf("%s (Issuer: %s)", cert.Subject.CommonName, cert.Issuer.String())
if issue != nil && issue.certIndex == idx {
label = fmt.Sprintf("%s [UNTRUSTED: %s]", cert.Subject.CommonName, issue.reason)
}
caChain = append(caChain, label)
fpSHA1, fpSHA256 := fingerprints(cert)
caChain = append(caChain, fmt.Sprintf("Fingerprint:\n\t%v (SHA1)\n\t%v (SHA256)", fpSHA1, fpSHA256))
return strings.Join(caChain, "\n")
}
func fmtFingerprint(fp []byte) string {
var fmtd []string
for _, x := range fp {
fmtd = append(fmtd, fmt.Sprintf("%02x", x))
}
return strings.Join(fmtd, ":")
}
func fingerprints(cert *x509.Certificate) (string, string) {
fpSHA1 := sha1.Sum(cert.Raw)
fpSHA256 := sha256.Sum256(cert.Raw)
return fmtFingerprint(fpSHA1[:]), fmtFingerprint(fpSHA256[:])
}
func sanBuild[S SAN](table *tabulate.Tabulate, sanList []S) {
if len(sanList) == 0 {
return
}
sanType := "DNS"
if reflect.TypeOf(sanList[0]).String() == "net.IP" {
sanType = "IP"
}
for i := 0; i < len(sanList); i += sanGrouping {
row := table.Row()
for x := range sanGrouping {
if i+x > len(sanList)-1 {
break
}
row.Column(fmt.Sprintf("%s:%s", sanType, sanList[i+x]))
}
}
}
func parseUrl(urlStr string, addr string, conf *tls.Config) (string, int) {
if !strings.Contains(urlStr, "://") {
urlStr = fmt.Sprintf("https://%s", urlStr)
}
components, err := url.Parse(urlStr)
if err != nil {
fmt.Print("ERROR: Could not parse provided URL\n")
os.Exit(1)
}
address := components.Hostname()
if addr != "" {
conf.ServerName = address
address = addr
}
port, err := strconv.Atoi(components.Port())
if err != nil {
switch components.Scheme {
case "http":
port = 80
default:
port = 443
}
}
return address, port
}
func buildConnection(cli args) net.Conn {
conf := &tls.Config{
InsecureSkipVerify: true,
}
address, port := parseUrl(cli.url, cli.address, conf)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dialer := tls.Dialer{Config: conf}
conn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", address, port))
if err != nil {
fmt.Printf("ERROR: Unable to connect: %s\n", err)
os.Exit(1)
}
return conn
}
func isSelfSigned(cert *x509.Certificate) bool {
return cert.CheckSignatureFrom(cert) == nil
}
func parseArgs() args {
var address string
flag.StringVarP(&address, "address", "a", "", "Connect to this address instead and use the URL hostname for the SNI value")
flag.Usage = func() {
fmt.Printf("Usage:\n")
fmt.Printf(" %s <url> [flags]\n\n", os.Args[0])
fmt.Printf("Arguments:\n")
fmt.Printf(" url\tURL to connect to\n\n")
fmt.Printf("Flags:\n")
flag.PrintDefaults()
}
flag.Parse()
cliArgs := flag.Args()
if len(cliArgs) < 1 {
fmt.Print("ERROR: URL was not provided\n\n")
flag.Usage()
os.Exit(1)
}
return args{
url: cliArgs[0],
address: address,
}
}
func buildTable(leaf *x509.Certificate, certs []*x509.Certificate, rootCert *x509.Certificate, verifyErr error) *tabulate.Tabulate {
sanTable := tabulate.New(tabulate.CompactPlain)
sanBuild(sanTable, leaf.DNSNames)
sanBuild(sanTable, leaf.IPAddresses)
certTable := tabulate.New(tabulate.CompactPlain)
row := certTable.Row()
row.Column("Common name")
row.Column(leaf.Subject.CommonName)
row = certTable.Row()
row.Column("Organization")
if len(leaf.Subject.Organization) > 0 {
text := leaf.Subject.Organization[0]
if len(leaf.Subject.Locality) > 0 {
text += ", " + leaf.Subject.Locality[0]
}
if len(leaf.Subject.Country) > 0 {
text += ", " + leaf.Subject.Country[0]
}
row.Column(text)
} else {
row.Column(fmt.Sprintf("[not provided: %s]", leaf.Subject.String()))
}
row = certTable.Row()
row.Column(fmt.Sprintf("SANs (%d)", len(leaf.DNSNames)+len(leaf.IPAddresses)))
row.Column(sanTable.String())
row = certTable.Row()
row.Column("Valid from")
row.Column(leaf.NotBefore.Format("2006-01-02 15:04:05 MST"))
row = certTable.Row()
row.Column("Valid to")
row.Column(leaf.NotAfter.Format("2006-01-02 15:04:05 MST"))
fpSHA1, fpSHA256 := fingerprints(leaf)
row = certTable.Row()
row.Column("Fingerprint")
row.Column(fmt.Sprintf("%v (SHA1)\n%v (SHA256)", fpSHA1, fpSHA256))
row = certTable.Row()
row.Column("Issuer")
row.Column(fmt.Sprintf("%s (%s)", leaf.Issuer.CommonName, leaf.Issuer.String()))
var issue *chainIssue
if verifyErr != nil {
issue = diagnoseChain(certs)
}
var caChain []string
for i, cert := range certs[1:] {
caChain = append(caChain, fmtChaincert(cert, issue, i+1))
}
// Only append rootCert if it wasn't already included in the certs slice
if rootCert != leaf && (len(certs) == 0 || rootCert != certs[len(certs)-1]) {
caChain = append(caChain, fmtChaincert(rootCert, issue, len(certs)))
}
row = certTable.Row()
row.Column("CA chain")
row.Column(strings.Join(caChain, "\n"))
return certTable
}
func main() {
cli := parseArgs()
conn := buildConnection(cli)
tlsConn, ok := conn.(*tls.Conn)
if !ok {
fmt.Println("ERROR: Connection is not a TLS connection")
os.Exit(1)
}
defer conn.Close()
certs := tlsConn.ConnectionState().PeerCertificates
if len(certs) == 0 {
fmt.Println("ERROR: No certificates received from server")
os.Exit(1)
}
leaf := certs[0]
intermediates := x509.NewCertPool()
for _, cert := range certs[1:] {
intermediates.AddCert(cert)
}
var rootCert *x509.Certificate
var verifyErr error
roots, err := x509.SystemCertPool()
if err != nil {
roots = x509.NewCertPool()
}
chains, err := leaf.Verify(x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
})
if err != nil {
verifyErr = err
if isSelfSigned(leaf) {
rootCert = leaf
} else {
if len(certs) > 1 {
rootCert = certs[len(certs)-1]
} else {
rootCert = leaf
}
}
} else {
rootCert = chains[0][len(chains[0])-1]
}
certTable := buildTable(leaf, certs, rootCert, verifyErr)
certTable.Print(os.Stdout)
}