200 lines
4.5 KiB
Go
200 lines
4.5 KiB
Go
package inspect
|
||
|
||
import (
|
||
"context"
|
||
"crypto/tls"
|
||
"errors"
|
||
"io"
|
||
"net"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/mattn/go-isatty"
|
||
"github.com/spf13/cobra"
|
||
"src.lwithers.me.uk/go/rsa/pkg/inspect"
|
||
"src.lwithers.me.uk/go/stdinprompt"
|
||
)
|
||
|
||
var (
|
||
outputFormat string = "text"
|
||
display func(src string, info []inspect.Info)
|
||
noPEMData = errors.New("no PEM data found")
|
||
)
|
||
|
||
// Register the "inspect" subcommand.
|
||
func Register(root *cobra.Command) {
|
||
cmd := &cobra.Command{
|
||
Use: "inspect source [source2 …]",
|
||
Short: "Inspect files and TLS servers",
|
||
Long: `Allows inspection of RSA keys, X.509 certificates, CRLs and CSRs. Pass
|
||
it a list of sources. Valid sources are:
|
||
• filenames
|
||
• ‘-’ for stdin
|
||
• host:port for extracting the server's certificate chain from a TLS handshake
|
||
• https://addr.example/ as per host:port
|
||
|
||
The output will by default be human-readable formatted text for the terminal,
|
||
but JSON and YAML are also supported through the --output flag.`,
|
||
|
||
RunE: Inspect,
|
||
Args: cobra.MinimumNArgs(1),
|
||
}
|
||
|
||
if isatty.IsTerminal(1) {
|
||
outputFormat = "coloured"
|
||
}
|
||
|
||
cmd.Flags().StringVarP(&outputFormat, "output", "o", outputFormat,
|
||
"Output format (coloured, text, json, yaml)")
|
||
|
||
root.AddCommand(cmd)
|
||
}
|
||
|
||
// Inspect PEM data from files or TLS handshakes.
|
||
func Inspect(cmd *cobra.Command, args []string) error {
|
||
setupAura(false)
|
||
switch outputFormat {
|
||
case "coloured":
|
||
setupAura(true)
|
||
display = displayText
|
||
case "text":
|
||
display = displayText
|
||
case "json":
|
||
display = displayStructured
|
||
defer displayStructuredJSON()
|
||
case "yaml":
|
||
display = displayStructured
|
||
defer displayStructuredYAML()
|
||
default:
|
||
return errors.New("invalid --output format (try: coloured, text, json or yaml)")
|
||
}
|
||
|
||
ok := true
|
||
for _, arg := range args {
|
||
switch {
|
||
case arg == "-":
|
||
ok = inspectStdin() && ok
|
||
|
||
case strings.HasPrefix(arg, "https://"):
|
||
ok = inspectHTTPS(arg) && ok
|
||
|
||
case strings.IndexByte(arg, '/') == -1 && strings.IndexByte(arg, ':') != -1:
|
||
_, _, err := net.SplitHostPort(arg)
|
||
if err == nil {
|
||
ok = inspectHost(arg) && ok
|
||
break
|
||
}
|
||
fallthrough
|
||
|
||
default:
|
||
ok = inspectFile(arg) && ok
|
||
}
|
||
}
|
||
if !ok {
|
||
os.Exit(1)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func inspectStdin() (ok bool) {
|
||
in := stdinprompt.New()
|
||
raw, err := io.ReadAll(in)
|
||
if err != nil {
|
||
displayErr(err, "-")
|
||
return false
|
||
}
|
||
|
||
info := inspect.LoadPEM(raw)
|
||
if len(info) == 0 {
|
||
displayErr(noPEMData, "-")
|
||
return false
|
||
}
|
||
|
||
display("(stdin)", info)
|
||
return true
|
||
}
|
||
|
||
func inspectHTTPS(addr string) (ok bool) {
|
||
u, err := url.Parse(addr)
|
||
if err != nil {
|
||
displayErr(err, addr)
|
||
return false
|
||
}
|
||
|
||
addr = u.Host
|
||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||
addr += ":443"
|
||
}
|
||
return inspectHost(addr)
|
||
}
|
||
|
||
func inspectHost(addr string) (ok bool) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
conn, err := tls.Dial("tcp", addr, &tls.Config{
|
||
// we want to skip verification so we can inspect invalid certs too
|
||
InsecureSkipVerify: true,
|
||
|
||
// we only enable RSA ciphersuites, but we do allow old ones, again
|
||
// so that we can report about bad / invalid servers
|
||
CipherSuites: []uint16{
|
||
tls.TLS_RSA_WITH_RC4_128_SHA,
|
||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
|
||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||
tls.TLS_AES_128_GCM_SHA256,
|
||
tls.TLS_AES_256_GCM_SHA384,
|
||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||
},
|
||
|
||
// we enable TLSv1.0 so that we can complain about it
|
||
MinVersion: tls.VersionTLS10,
|
||
})
|
||
if err != nil {
|
||
displayErr(err, addr)
|
||
return false
|
||
}
|
||
|
||
if err := conn.HandshakeContext(ctx); err != nil {
|
||
displayErr(err, addr)
|
||
return false
|
||
}
|
||
|
||
display(addr, inspect.ConnectionState(conn.ConnectionState()))
|
||
return true
|
||
}
|
||
|
||
func inspectFile(filename string) (ok bool) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
displayErr(err, filename)
|
||
return false
|
||
}
|
||
|
||
info := inspect.LoadPEM(raw)
|
||
if len(info) == 0 {
|
||
displayErr(noPEMData, filename)
|
||
return false
|
||
}
|
||
|
||
display(filename, info)
|
||
return true
|
||
}
|
||
|
||
func displayYAML(src string, info []inspect.Info) {
|
||
}
|