323 lines
7.2 KiB
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)
|
|
}
|