2023-03-01 21:30:16 +00:00
|
|
|
|
package inspect
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"errors"
|
2023-04-29 10:59:15 +01:00
|
|
|
|
"io"
|
2023-03-01 21:30:16 +00:00
|
|
|
|
"net"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/mattn/go-isatty"
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
"src.lwithers.me.uk/go/rsa/pkg/inspect"
|
2023-04-29 10:59:15 +01:00
|
|
|
|
"src.lwithers.me.uk/go/stdinprompt"
|
2023-03-01 21:30:16 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
outputFormat string = "text"
|
|
|
|
|
display func(src string, info []inspect.Info)
|
2023-04-29 10:59:15 +01:00
|
|
|
|
noPEMData = errors.New("no PEM data found")
|
2023-03-01 21:30:16 +00:00
|
|
|
|
)
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
// Register the "inspect" subcommand.
|
2023-03-01 21:30:16 +00:00
|
|
|
|
func Register(root *cobra.Command) {
|
|
|
|
|
cmd := &cobra.Command{
|
2023-04-29 11:48:10 +01:00
|
|
|
|
Use: "inspect source [source2 …]",
|
2023-03-01 21:30:16 +00:00
|
|
|
|
Short: "Inspect files and TLS servers",
|
2023-04-29 11:48:10 +01:00
|
|
|
|
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),
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isatty.IsTerminal(1) {
|
|
|
|
|
outputFormat = "coloured"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.Flags().StringVarP(&outputFormat, "output", "o", outputFormat,
|
|
|
|
|
"Output format (coloured, text, json, yaml)")
|
|
|
|
|
|
|
|
|
|
root.AddCommand(cmd)
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
// Inspect PEM data from files or TLS handshakes.
|
2023-03-01 21:30:16 +00:00
|
|
|
|
func Inspect(cmd *cobra.Command, args []string) error {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
setupAura(false)
|
2023-03-01 21:30:16 +00:00
|
|
|
|
switch outputFormat {
|
|
|
|
|
case "coloured":
|
2023-04-29 10:59:15 +01:00
|
|
|
|
setupAura(true)
|
|
|
|
|
display = displayText
|
2023-03-01 21:30:16 +00:00
|
|
|
|
case "text":
|
|
|
|
|
display = displayText
|
|
|
|
|
case "json":
|
2023-04-29 10:59:15 +01:00
|
|
|
|
display = displayStructured
|
|
|
|
|
defer displayStructuredJSON()
|
2023-03-01 21:30:16 +00:00
|
|
|
|
case "yaml":
|
2023-04-29 10:59:15 +01:00
|
|
|
|
display = displayStructured
|
|
|
|
|
defer displayStructuredYAML()
|
2023-03-01 21:30:16 +00:00
|
|
|
|
default:
|
|
|
|
|
return errors.New("invalid --output format (try: coloured, text, json or yaml)")
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
ok := true
|
2023-03-01 21:30:16 +00:00
|
|
|
|
for _, arg := range args {
|
|
|
|
|
switch {
|
|
|
|
|
case arg == "-":
|
2023-04-29 10:59:15 +01:00
|
|
|
|
ok = inspectStdin() && ok
|
2023-03-01 21:30:16 +00:00
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(arg, "https://"):
|
2023-04-29 10:59:15 +01:00
|
|
|
|
ok = inspectHTTPS(arg) && ok
|
2023-03-01 21:30:16 +00:00
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
case strings.IndexByte(arg, '/') == -1 && strings.IndexByte(arg, ':') != -1:
|
2023-03-01 21:30:16 +00:00
|
|
|
|
_, _, err := net.SplitHostPort(arg)
|
|
|
|
|
if err == nil {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
ok = inspectHost(arg) && ok
|
2023-03-01 21:30:16 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
fallthrough
|
|
|
|
|
|
|
|
|
|
default:
|
2023-04-29 10:59:15 +01:00
|
|
|
|
ok = inspectFile(arg) && ok
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-29 10:59:15 +01:00
|
|
|
|
if !ok {
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2023-03-01 21:30:16 +00:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
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
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
func inspectHTTPS(addr string) (ok bool) {
|
2023-03-01 21:30:16 +00:00
|
|
|
|
u, err := url.Parse(addr)
|
|
|
|
|
if err != nil {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
displayErr(err, addr)
|
|
|
|
|
return false
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
addr = u.Host
|
|
|
|
|
if _, _, err := net.SplitHostPort(addr); err != nil {
|
|
|
|
|
addr += ":443"
|
|
|
|
|
}
|
|
|
|
|
return inspectHost(addr)
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
func inspectHost(addr string) (ok bool) {
|
2023-03-01 21:30:16 +00:00
|
|
|
|
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 {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
displayErr(err, addr)
|
|
|
|
|
return false
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := conn.HandshakeContext(ctx); err != nil {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
displayErr(err, addr)
|
|
|
|
|
return false
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display(addr, inspect.ConnectionState(conn.ConnectionState()))
|
2023-04-29 10:59:15 +01:00
|
|
|
|
return true
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-04-29 10:59:15 +01:00
|
|
|
|
func inspectFile(filename string) (ok bool) {
|
2023-03-01 21:30:16 +00:00
|
|
|
|
raw, err := os.ReadFile(filename)
|
|
|
|
|
if err != nil {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
displayErr(err, filename)
|
|
|
|
|
return false
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info := inspect.LoadPEM(raw)
|
|
|
|
|
if len(info) == 0 {
|
2023-04-29 10:59:15 +01:00
|
|
|
|
displayErr(noPEMData, filename)
|
|
|
|
|
return false
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display(filename, info)
|
2023-04-29 10:59:15 +01:00
|
|
|
|
return true
|
2023-03-01 21:30:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func displayYAML(src string, info []inspect.Info) {
|
|
|
|
|
}
|