rsa/cmd/inspect/inspect.go

190 lines
4.0 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",
Short: "Inspect files and TLS servers",
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) {
}