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