refactor certigo package main

This splits the logic into a cli, which handles parsing command line args and
reading files, and a terminal abstraction for handling user input and ouput.

All uses of os.Exit are removed in favor of returning errors.

Overall this enables better testing and reuse of code.  Previously we had to
rely on external unit testing for CLI tests, which are harder to write tests.
This commit is contained in:
Matthew McPherrin
2020-01-13 15:58:53 -08:00
parent 80714936c4
commit 26cd65d9bd
9 changed files with 525 additions and 285 deletions
+231
View File
@@ -0,0 +1,231 @@
package cli
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"github.com/square/certigo/cli/terminal"
"github.com/square/certigo/lib"
"github.com/square/certigo/starttls"
"gopkg.in/alecthomas/kingpin.v2"
"os"
"strings"
)
var (
app = kingpin.New("certigo", "A command-line utility to examine and validate certificates to help with debugging SSL/TLS issues.")
verbose = app.Flag("verbose", "Print verbose").Short('v').Bool()
dump = app.Command("dump", "Display information about a certificate from a file or stdin.")
dumpFiles = dump.Arg("file", "Certificate file to dump (or stdin if not specified).").ExistingFiles()
dumpType = dump.Flag("format", "Format of given input (PEM, DER, JCEKS, PKCS12; heuristic if missing).").Short('f').String()
dumpPassword = dump.Flag("password", "Password for PKCS12/JCEKS key stores (reads from TTY if missing).").Short('p').String()
dumpPem = dump.Flag("pem", "Write output as PEM blocks instead of human-readable format.").Short('m').Bool()
dumpJSON = dump.Flag("json", "Write output as machine-readable JSON format.").Short('j').Bool()
connect = app.Command("connect", "Connect to a server and print its certificate(s).")
connectTo = connect.Arg("server[:port]", "Hostname or IP to connect to, with optional port.").Required().String()
connectName = connect.Flag("name", "Override the server name used for Server Name Indication (SNI).").Short('n').String()
connectCaPath = connect.Flag("ca", "Path to CA bundle (system default if unspecified).").ExistingFile()
connectCert = connect.Flag("cert", "Client certificate chain for connecting to server (PEM).").ExistingFile()
connectKey = connect.Flag("key", "Private key for client certificate, if not in same file (PEM).").ExistingFile()
connectStartTLS = connect.Flag("start-tls", fmt.Sprintf("Enable StartTLS protocol; one of: %v.", starttls.Protocols)).Short('t').PlaceHolder("PROTOCOL").Enum(starttls.Protocols...)
connectIdentity = connect.Flag("identity", "With --start-tls, sets the DB user or SMTP EHLO name").Default("certigo").String()
connectProxy = connect.Flag("proxy", "Optional URI for HTTP(s) CONNECT proxy to dial connections with").URL()
connectTimeout = connect.Flag("timeout", "Timeout for connecting to remote server (can be '5m', '1s', etc).").Default("5s").Duration()
connectPem = connect.Flag("pem", "Write output as PEM blocks instead of human-readable format.").Short('m').Bool()
connectJSON = connect.Flag("json", "Write output as machine-readable JSON format.").Short('j').Bool()
connectVerify = connect.Flag("verify", "Verify certificate chain.").Bool()
verify = app.Command("verify", "Verify a certificate chain from file/stdin against a name.")
verifyFile = verify.Arg("file", "Certificate file to dump (or stdin if not specified).").ExistingFile()
verifyType = verify.Flag("format", "Format of given input (PEM, DER, JCEKS, PKCS12; heuristic if missing).").Short('f').String()
verifyPassword = verify.Flag("password", "Password for PKCS12/JCEKS key stores (reads from TTY if missing).").Short('p').String()
verifyName = verify.Flag("name", "Server name to verify certificate against.").Short('n').Required().String()
verifyCaPath = verify.Flag("ca", "Path to CA bundle (system default if unspecified).").ExistingFile()
verifyJSON = verify.Flag("json", "Write output as machine-readable JSON format.").Short('j').Bool()
)
func Run(args []string, tty terminal.Terminal) int {
terminalWidth := tty.DetermineWidth()
stdout := tty.Output()
errOut := tty.Error()
printErr := func(format string, args ...interface{}) int {
_, err := fmt.Fprintf(errOut, format, args...)
if err != nil {
// If we can't write the error, we bail with a different return code... not much good
// we can do at this point
return 3
}
return 2
}
app.Version("1.11.0")
// Alias starttls to start-tls
connect.Flag("starttls", "").Hidden().EnumVar(connectStartTLS, starttls.Protocols...)
// Use long help because many useful flags are under subcommands
app.UsageTemplate(kingpin.LongHelpTemplate)
result := lib.SimpleResult{}
command, err := app.Parse(args)
if err != nil {
return printErr("%s, try --help\n", err)
}
switch command {
case dump.FullCommand(): // Dump certificate
if dumpPassword != nil && *dumpPassword != "" {
tty.SetDefaultPassword(*dumpPassword)
}
files, err := inputFiles(*dumpFiles)
defer func() {
for _, file := range files {
file.Close()
}
}()
if *dumpPem {
err = lib.ReadAsPEMFromFiles(files, *dumpType, tty.ReadPassword, func(block *pem.Block) error {
block.Headers = nil
return pem.Encode(stdout, block)
})
} else {
err = lib.ReadAsX509FromFiles(files, *dumpType, tty.ReadPassword, func(cert *x509.Certificate, err error) error {
if err != nil {
return fmt.Errorf("error parsing block: %s\n", strings.TrimSuffix(err.Error(), "\n"))
} else {
result.Certificates = append(result.Certificates, cert)
}
return nil
})
if *dumpJSON {
blob, _ := json.Marshal(result)
fmt.Println(string(blob))
} else {
for i, cert := range result.Certificates {
fmt.Fprintf(stdout, "** CERTIFICATE %d **\n", i+1)
fmt.Fprintf(stdout, "%s\n\n", lib.EncodeX509ToText(cert, terminalWidth, *verbose))
}
}
}
if err != nil {
return printErr("error: %s\n", strings.TrimSuffix(err.Error(), "\n"))
} else if len(result.Certificates) == 0 && !*dumpPem {
printErr("warning: no certificates found in input\n")
}
case connect.FullCommand(): // Get certs by connecting to a server
if connectStartTLS == nil && connectIdentity != nil {
return printErr("error: --identity can only be used with --start-tls")
}
connState, cri, err := starttls.GetConnectionState(
*connectStartTLS, *connectName, *connectTo, *connectIdentity,
*connectCert, *connectKey, *connectProxy, *connectTimeout)
if err != nil {
return printErr("%s\n", strings.TrimSuffix(err.Error(), "\n"))
}
result.TLSConnectionState = connState
result.CertificateRequestInfo = cri
for _, cert := range connState.PeerCertificates {
if *connectPem {
pem.Encode(stdout, lib.EncodeX509ToPEM(cert, nil))
} else {
result.Certificates = append(result.Certificates, cert)
}
}
var hostname string
if *connectName != "" {
hostname = *connectName
} else {
hostname = strings.Split(*connectTo, ":")[0]
}
verifyResult := lib.VerifyChain(connState.PeerCertificates, connState.OCSPResponse, hostname, *connectCaPath)
result.VerifyResult = &verifyResult
if *connectJSON {
blob, _ := json.Marshal(result)
fmt.Println(string(blob))
} else if !*connectPem {
fmt.Fprintf(
stdout, "%s\n\n",
lib.EncodeTLSInfoToText(result.TLSConnectionState, result.CertificateRequestInfo))
for i, cert := range result.Certificates {
fmt.Fprintf(stdout, "** CERTIFICATE %d **\n", i+1)
fmt.Fprintf(stdout, "%s\n\n", lib.EncodeX509ToText(cert, terminalWidth, *verbose))
}
lib.PrintVerifyResult(stdout, *result.VerifyResult)
}
if *connectVerify && len(result.VerifyResult.Error) > 0 {
return 1
}
case verify.FullCommand():
if verifyPassword != nil && *verifyPassword != "" {
tty.SetDefaultPassword(*verifyPassword)
}
file, err := inputFile(*verifyFile)
if err != nil {
return printErr("%s\n", err.Error())
}
defer file.Close()
chain := []*x509.Certificate{}
err = lib.ReadAsX509FromFiles([]*os.File{file}, *verifyType, tty.ReadPassword, func(cert *x509.Certificate, err error) error {
if err != nil {
return err
} else {
chain = append(chain, cert)
}
return nil
})
if err != nil {
return printErr("error parsing block: %s\n", strings.TrimSuffix(err.Error(), "\n"))
}
verifyResult := lib.VerifyChain(chain, nil, *verifyName, *verifyCaPath)
if *verifyJSON {
blob, _ := json.Marshal(verifyResult)
fmt.Println(string(blob))
} else {
lib.PrintVerifyResult(stdout, verifyResult)
}
if verifyResult.Error != "" {
return 1
}
}
return 0
}
func inputFile(fileName string) (*os.File, error) {
if fileName == "" {
return os.Stdin, nil
}
rawFile, err := os.Open(fileName)
if err != nil {
return nil, fmt.Errorf("unable to open file: %s\n", err)
}
return rawFile, nil
}
func inputFiles(fileNames []string) ([]*os.File, error) {
var files []*os.File
if fileNames != nil {
for _, filename := range fileNames {
rawFile, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("unable to open file: %s\n", err)
}
files = append(files, rawFile)
}
} else {
files = append(files, os.Stdin)
}
return files, nil
}
+94
View File
@@ -0,0 +1,94 @@
package cli
import (
"io/ioutil"
"os"
"testing"
"github.com/square/certigo/cli/terminal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testCert string = `
-----BEGIN CERTIFICATE-----
MIIE1DCCArygAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzELMAkGA1UEBhMCVVMx
FzAVBgNVBAoMDnRlc3QxLmFjbWUuY29tMRcwFQYDVQQDDA5JbnRlcm1lZGlhZXRD
QTAeFw0xNzA3MTkxNjUwMjBaFw0xNzA3MjkxNjUwMjBaMDUxCzAJBgNVBAYTAlVT
MRcwFQYDVQQKDA50ZXN0MS5hY21lLmNvbTENMAsGA1UEAwwEYmxvZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKm8P47lABp4+rz2nN+QYrxedbaFVWoF
FuoSkqcHsafMwbMrN+kI6wJVtlbwviDvxWFJ92q0H71QNFybTsmof3KUN/kYCp7P
+LKhBrN0ttWI5q6v5eDrjN0VdtVdnlZOYmJFbvETOgfK/qXKNRRM8HYW0tdqrtEw
CR5dIu53xVUSViBdwXpuy2c5W2mFn1gxTpdW+3hbZsL1pHrU9qPWLtTgl/KY8kjs
I7KW1cIcinE4SJomhB5L/4emhxKGY+kEa2+fN9IPjjvKSMOw9kiBKk1GHZcIY5EA
O3TIfUk3fysPzi5qA0su/bNtPQy1uXgXS10xUlV7pqRPvHjiNzgFkXUCAwEAAaOB
4zCB4DAJBgNVHRMEAjAAMB0GA1UdDgQWBBRVQ91jSOONzVr1VGBdJOlPN+3XxTBg
BgNVHSMEWTBXgBQ13bfx50rDZO3y2CZdHPgleFUEoKE7pDkwNzELMAkGA1UEBhMC
VVMxFzAVBgNVBAoMDnRlc3QxLmFjbWUuY29tMQ8wDQYDVQQDDAZSb290Q0GCAhAA
MA4GA1UdDwEB/wQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAtBgNVHREEJjAk
hiJzcGlmZmU6Ly9kZXYuYWNtZS5jb20vcGF0aC9zZXJ2aWNlMA0GCSqGSIb3DQEB
CwUAA4ICAQBp2+rtUxt1VmNM/vi6PwoSoYzWFmQ2nc4OM7bsOG4uppU54wRYZ+T7
c42EcrpyBgWn+rWHT1Hi6SNcmloKHydaUTZ4pq3IlKKnBNqwivU5BzIxYLDrhR/U
wd9s1tgmLvADqkQa1XjjSFn5Auoj1R640ry4qpw8IOusdm6wVhru4ssRnHX4E2uR
jQe7b3ws38aZhjtL78Ip0BB4yPxWJRp/WmEoT33QP+cZhA4IYWECxNODr6DSJeq2
VNu/6JACGrNfM2Sjt4Wxz+nIa3cKDNCA6PR8StTUTcoQ6ZBzpn+n/Q1xSRIOJz6N
hgfkyb9O7HAMdAP+TxehjqG3gh5Ky2DgYMCIZOztVzsuOb1DGJe/kGUKeRJLl2/O
QwkctwUOcVIxckNu6OvclriFzvoXObqO77XeCI2V1Vef0wGTWlWNOdbFa4708Y7f
5UdwInYQUi87RFDnc1SDU4Jrsv4KzZiv9FCfDg8pCBIdWpWT7DAuI0d7i7PZ+iFt
ZZ6sb/YDkyiDXU4ar/dja0FDE2r7jsN9D+FfW49+iDvXr4ELQyhZpW3Zr1Ojwm58
CJzjZwbRYiVwPBRsKmiYfO1E7esvw3CmjK5chfz8c40f6/APDro9ZmYNBRv2CnJy
t/DtcM/GpAhBbLP9Tk7kPB41v5fRIxVDo50Iz/qvkr37pQ4RsejSFg==
-----END CERTIFICATE-----
`
const expectedVerbose string = `** CERTIFICATE 1 **
Serial: 4096
Valid: 2017-07-19 16:50 UTC to 2017-07-29 16:50 UTC
Signature: SHA256-RSA
Subject Info:
Country: US
Organization: test1.acme.com
CommonName: blog
Issuer Info:
Country: US
Organization: test1.acme.com
CommonName: IntermediaetCA
Subject Key ID: 55:43:DD:63:48:E3:8D:CD:5A:F5:54:60:5D:24:E9:4F:37:ED:D7:C5
Authority Key ID: 35:DD:B7:F1:E7:4A:C3:64:ED:F2:D8:26:5D:1C:F8:25:78:55:04:A0
Basic Constraints: CA:false
Key Usage:
Digital Signature
Key Encipherment
Key Agreement
Extended Key Usage:
Server Auth
URI Names:
spiffe://dev.acme.com/path/service
`
// Test basic dump functionality: Dump a cert
func TestDump(t *testing.T) {
tmpfile, err := ioutil.TempFile("", t.Name())
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
_, err = tmpfile.Write([]byte(testCert))
require.NoError(t, err)
args := []string{"dump", "--verbose", "--format", "PEM", tmpfile.Name()}
testTerminal := terminal.TestTerminal{Width: 80}
assert.EqualValues(t, 0, Run(args, &testTerminal), "process should exit 0")
assert.Empty(t, testTerminal.ErrorBuf.Bytes(), "no error output expected")
assert.EqualValues(t, expectedVerbose, testTerminal.OutputBuf.String())
}
func TestDumpMissingFile(t *testing.T) {
testTerminal := terminal.TestTerminal{Width: 80}
args := []string{"dump", "this-is-a-file-that-definitely-does-not-exist1111.pem"}
assert.EqualValues(t, 2, Run(args, &testTerminal), "process should exit 0")
const expected = "path 'this-is-a-file-that-definitely-does-not-exist1111.pem' does not exist, try --help\n"
assert.Equal(t, expected, testTerminal.ErrorBuf.String())
assert.Empty(t, testTerminal.OutputBuf.Bytes())
}
+97
View File
@@ -0,0 +1,97 @@
package terminal
import (
"fmt"
"io"
"os"
"strings"
"github.com/mattn/go-colorable"
"golang.org/x/crypto/ssh/terminal"
)
const minWidth = 60
const maxWidth = 80
// Terminal handles interacting with the user in Certigo
type Terminal interface {
Output() io.Writer
Error() io.Writer
SetDefaultPassword(password string)
ReadPassword(prompt string) string
DetermineWidth() int
}
// TTY represents unixish stdio, possibly with /dev/tty used to read user input
type TTY struct {
defaultPassword *string
}
func OpenTTY() *TTY {
return &TTY{}
}
func (t *TTY) Output() io.Writer {
return colorable.NewColorableStdout()
}
func (t *TTY) Error() io.Writer {
return os.Stderr
}
func (t *TTY) SetDefaultPassword(password string) {
t.defaultPassword = &password
}
func (t *TTY) ReadPassword(prompt string) string {
if t.defaultPassword != nil {
return *t.defaultPassword
}
var tty *os.File
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
tty = os.Stdin
} else {
defer tty.Close()
}
tty.WriteString("Enter password")
if prompt != "" {
tty.WriteString(fmt.Sprintf(" for entry [%s]", prompt))
}
tty.WriteString(": ")
password, err := terminal.ReadPassword(int(tty.Fd()))
tty.WriteString("\n")
if err != nil {
fmt.Fprintf(os.Stderr, "error reading password: %s\n", err)
os.Exit(1)
}
return strings.TrimSuffix(string(password), "\n")
}
func (t *TTY) DetermineWidth() int {
var width int
fd := int(os.Stdout.Fd())
if terminal.IsTerminal(fd) {
var err error
width, _, err = terminal.GetSize(fd)
if err != nil {
width = minWidth
}
} else {
width = minWidth
}
if width > maxWidth {
width = maxWidth
} else if width < minWidth {
width = minWidth
}
return width
}
// Assert TTY implements terminal
var _ Terminal = &TTY{}
+39
View File
@@ -0,0 +1,39 @@
package terminal
import (
"bytes"
"io"
"github.com/mattn/go-colorable"
)
// TestTerminal just collects input into buffers
// That can be used to check output in tests
type TestTerminal struct {
OutputBuf bytes.Buffer
ErrorBuf bytes.Buffer
Password string
Width int
}
var _ Terminal = &TestTerminal{}
func (t *TestTerminal) Output() io.Writer {
return colorable.NewNonColorable(&t.OutputBuf)
}
func (t *TestTerminal) Error() io.Writer {
return &t.ErrorBuf
}
func (t *TestTerminal) SetDefaultPassword(password string) {
t.Password = password
}
func (t *TestTerminal) ReadPassword(prompt string) string {
return t.Password
}
func (t TestTerminal) DetermineWidth() int {
return t.Width
}
+2 -1
View File
@@ -12,10 +12,11 @@ require (
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-isatty v0.0.11
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mwitkow/go-http-dialer v0.0.0-20161116154839-378f744fb2b8
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225
+44 -19
View File
@@ -88,8 +88,8 @@ func errorFromErrors(errs []error) error {
// data may be in plain-text PEM files, DER-encoded certificates or PKCS7
// envelopes, or PKCS12/JCEKS keystores. All inputs will be converted to PEM
// blocks and passed to the callback.
func ReadAsPEMFromFiles(files []*os.File, format string, password func(string) string, callback func(*pem.Block)) error {
errs := []error{}
func ReadAsPEMFromFiles(files []*os.File, format string, password func(string) string, callback func(*pem.Block) error) error {
var errs []error
for _, file := range files {
reader := bufio.NewReaderSize(file, 4)
format, err := formatForFile(reader, file.Name(), format)
@@ -109,7 +109,7 @@ func ReadAsPEMFromFiles(files []*os.File, format string, password func(string) s
// be in plain-text PEM files, DER-encoded certificates or PKCS7 envelopes, or
// PKCS12/JCEKS keystores. All inputs will be converted to PEM blocks and
// passed to the callback.
func ReadAsPEM(readers []io.Reader, format string, password func(string) string, callback func(*pem.Block)) error {
func ReadAsPEM(readers []io.Reader, format string, password func(string) string, callback func(*pem.Block) error) error {
errs := []error{}
for _, r := range readers {
reader := bufio.NewReaderSize(r, 4)
@@ -130,7 +130,7 @@ func ReadAsPEM(readers []io.Reader, format string, password func(string) string,
// inputs. Input data may be in plain-text PEM files, DER-encoded certificates
// or PKCS7 envelopes, or PKCS12/JCEKS keystores. All inputs will be converted
// to X.509 certificates (private keys are skipped) and passed to the callback.
func ReadAsX509FromFiles(files []*os.File, format string, password func(string) string, callback func(*x509.Certificate, error)) error {
func ReadAsX509FromFiles(files []*os.File, format string, password func(string) string, callback func(*x509.Certificate, error) error) error {
errs := []error{}
for _, file := range files {
reader := bufio.NewReaderSize(file, 4)
@@ -151,7 +151,7 @@ func ReadAsX509FromFiles(files []*os.File, format string, password func(string)
// data may be in plain-text PEM files, DER-encoded certificates or PKCS7
// envelopes, or PKCS12/JCEKS keystores. All inputs will be converted to X.509
// certificates (private keys are skipped) and passed to the callback.
func ReadAsX509(readers []io.Reader, format string, password func(string) string, callback func(*x509.Certificate, error)) error {
func ReadAsX509(readers []io.Reader, format string, password func(string) string, callback func(*x509.Certificate, error) error) error {
errs := []error{}
for _, r := range readers {
reader := bufio.NewReaderSize(r, 4)
@@ -168,27 +168,28 @@ func ReadAsX509(readers []io.Reader, format string, password func(string) string
return errorFromErrors(errs)
}
func pemToX509(callback func(*x509.Certificate, error)) func(*pem.Block) {
return func(block *pem.Block) {
func pemToX509(callback func(*x509.Certificate, error) error) func(*pem.Block) error {
return func(block *pem.Block) error {
switch block.Type {
case "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
callback(cert, err)
return callback(cert, err)
case "PKCS7":
certs, err := pkcs7.ExtractCertificates(block.Bytes)
if err == nil {
for _, cert := range certs {
callback(cert, nil)
return callback(cert, nil)
}
} else {
callback(nil, err)
return callback(nil, err)
}
}
return nil
}
}
// readCertsFromStream takes some input and converts it to PEM blocks.
func readCertsFromStream(reader io.Reader, filename string, format string, password func(string) string, callback func(*pem.Block)) error {
func readCertsFromStream(reader io.Reader, filename string, format string, password func(string) string, callback func(*pem.Block) error) error {
headers := map[string]string{}
if filename != "" && filename != os.Stdin.Name() {
headers[fileHeader] = filename
@@ -200,7 +201,10 @@ func readCertsFromStream(reader io.Reader, filename string, format string, passw
for scanner.Scan() {
block, _ := pem.Decode(scanner.Bytes())
block.Headers = mergeHeaders(block.Headers, headers)
callback(block)
err := callback(block)
if err != nil {
return err
}
}
return nil
case "DER":
@@ -211,14 +215,20 @@ func readCertsFromStream(reader io.Reader, filename string, format string, passw
x509Certs, err0 := x509.ParseCertificates(data)
if err0 == nil {
for _, cert := range x509Certs {
callback(EncodeX509ToPEM(cert, headers))
err := callback(EncodeX509ToPEM(cert, headers))
if err != nil {
return err
}
}
return nil
}
p7bBlocks, err1 := pkcs7.ParseSignedData(data)
if err1 == nil {
for _, block := range p7bBlocks {
callback(pkcs7ToPem(block, headers))
err := callback(pkcs7ToPem(block, headers))
if err != nil {
return err
}
}
return nil
}
@@ -234,7 +244,10 @@ func readCertsFromStream(reader io.Reader, filename string, format string, passw
}
for _, block := range blocks {
block.Headers = mergeHeaders(block.Headers, headers)
callback(block)
err := callback(block)
if err != nil {
return err
}
}
return nil
case "JCEKS":
@@ -244,20 +257,32 @@ func readCertsFromStream(reader io.Reader, filename string, format string, passw
}
for _, alias := range keyStore.ListCerts() {
cert, _ := keyStore.GetCert(alias)
callback(EncodeX509ToPEM(cert, mergeHeaders(headers, map[string]string{nameHeader: alias})))
err := callback(EncodeX509ToPEM(cert, mergeHeaders(headers, map[string]string{nameHeader: alias})))
if err != nil {
return err
}
}
for _, alias := range keyStore.ListPrivateKeys() {
key, certs, err := keyStore.GetPrivateKeyAndCerts(alias, []byte(password(alias)))
if err != nil {
return fmt.Errorf("unable to parse keystore: %s\n", err)
}
block, err := keyToPem(key, mergeHeaders(headers, map[string]string{nameHeader: alias}))
mergedHeaders := mergeHeaders(headers, map[string]string{nameHeader: alias})
block, err := keyToPem(key, mergedHeaders)
if err != nil {
return fmt.Errorf("problem reading key: %s\n", err)
}
callback(block)
if err := callback(block); err != nil {
return err
}
for _, cert := range certs {
callback(EncodeX509ToPEM(cert, mergeHeaders(headers, map[string]string{nameHeader: alias})))
if err = callback(EncodeX509ToPEM(cert, mergedHeaders)); err != nil {
return err
}
}
}
return nil
+14 -10
View File
@@ -76,15 +76,14 @@ func (s SimpleResult) MarshalJSON() ([]byte, error) {
return json.Marshal(out)
}
func caBundle(caPath string) *x509.CertPool {
func caBundle(caPath string) (*x509.CertPool, error) {
if caPath == "" {
return nil
return nil, nil
}
caFile, err := os.Open(caPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening CA bundle %s: %s\n", caPath, err)
os.Exit(1)
return nil, fmt.Errorf("error opening CA bundle %s: %s\n", caPath, err)
}
bundle := x509.NewCertPool()
@@ -95,18 +94,18 @@ func caBundle(caPath string) *x509.CertPool {
// TODO: The JDK trust store ships with this password.
return "changeit"
},
func(cert *x509.Certificate, err error) {
func(cert *x509.Certificate, err error) error {
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing CA bundle: %s\n", err)
return fmt.Errorf("error parsing CA bundle: %s\n", err)
} else {
bundle.AddCert(cert)
}
return nil
})
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing CA bundle: %s\n", err)
os.Exit(1)
return nil, fmt.Errorf("error parsing CA bundle: %s\n", err)
}
return bundle
return bundle, nil
}
func VerifyChain(certs []*x509.Certificate, ocspStaple []byte, dnsName, caPath string) SimpleVerification {
@@ -120,9 +119,14 @@ func VerifyChain(certs []*x509.Certificate, ocspStaple []byte, dnsName, caPath s
intermediates.AddCert(certs[i])
}
roots, err := caBundle(caPath)
if err != nil {
result.Error = fmt.Sprintf("%s", err)
return result
}
opts := x509.VerifyOptions{
DNSName: dnsName,
Roots: caBundle(caPath),
Roots: roots,
Intermediates: intermediates,
}
+3 -254
View File
@@ -17,263 +17,12 @@
package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"strings"
colorable "github.com/mattn/go-colorable"
"github.com/square/certigo/lib"
"github.com/square/certigo/starttls"
"golang.org/x/crypto/ssh/terminal"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/square/certigo/cli"
"github.com/square/certigo/cli/terminal"
)
var (
app = kingpin.New("certigo", "A command-line utility to examine and validate certificates to help with debugging SSL/TLS issues.")
verbose = app.Flag("verbose", "Print verbose").Short('v').Bool()
dump = app.Command("dump", "Display information about a certificate from a file or stdin.")
dumpFiles = dump.Arg("file", "Certificate file to dump (or stdin if not specified).").ExistingFiles()
dumpType = dump.Flag("format", "Format of given input (PEM, DER, JCEKS, PKCS12; heuristic if missing).").Short('f').String()
dumpPassword = dump.Flag("password", "Password for PKCS12/JCEKS key stores (reads from TTY if missing).").Short('p').String()
dumpPem = dump.Flag("pem", "Write output as PEM blocks instead of human-readable format.").Short('m').Bool()
dumpJSON = dump.Flag("json", "Write output as machine-readable JSON format.").Short('j').Bool()
connect = app.Command("connect", "Connect to a server and print its certificate(s).")
connectTo = connect.Arg("server[:port]", "Hostname or IP to connect to, with optional port.").String()
connectName = connect.Flag("name", "Override the server name used for Server Name Indication (SNI).").Short('n').String()
connectCaPath = connect.Flag("ca", "Path to CA bundle (system default if unspecified).").ExistingFile()
connectCert = connect.Flag("cert", "Client certificate chain for connecting to server (PEM).").ExistingFile()
connectKey = connect.Flag("key", "Private key for client certificate, if not in same file (PEM).").ExistingFile()
connectStartTLS = connect.Flag("start-tls", fmt.Sprintf("Enable StartTLS protocol; one of: %v.", starttls.Protocols)).Short('t').PlaceHolder("PROTOCOL").Enum(starttls.Protocols...)
connectIdentity = connect.Flag("identity", "With --start-tls, sets the DB user or SMTP EHLO name").Default("certigo").String()
connectProxy = connect.Flag("proxy", "Optional URI for HTTP(s) CONNECT proxy to dial connections with").URL()
connectTimeout = connect.Flag("timeout", "Timeout for connecting to remote server (can be '5m', '1s', etc).").Default("5s").Duration()
connectPem = connect.Flag("pem", "Write output as PEM blocks instead of human-readable format.").Short('m').Bool()
connectJSON = connect.Flag("json", "Write output as machine-readable JSON format.").Short('j').Bool()
connectVerify = connect.Flag("verify", "Verify certificate chain.").Bool()
verify = app.Command("verify", "Verify a certificate chain from file/stdin against a name.")
verifyFile = verify.Arg("file", "Certificate file to dump (or stdin if not specified).").ExistingFile()
verifyType = verify.Flag("format", "Format of given input (PEM, DER, JCEKS, PKCS12; heuristic if missing).").Short('f').String()
verifyPassword = verify.Flag("password", "Password for PKCS12/JCEKS key stores (reads from TTY if missing).").Short('p').String()
verifyName = verify.Flag("name", "Server name to verify certificate against.").Short('n').Required().String()
verifyCaPath = verify.Flag("ca", "Path to CA bundle (system default if unspecified).").ExistingFile()
verifyJSON = verify.Flag("json", "Write output as machine-readable JSON format.").Short('j').Bool()
)
const minWidth = 60
const maxWidth = 80
func main() {
app.Version("1.11.0")
terminalWidth := determineTerminalWidth()
// Alias starttls to start-tls
connect.Flag("starttls", "").Hidden().EnumVar(connectStartTLS, starttls.Protocols...)
// Use long help because many useful flags are under subcommands
app.UsageTemplate(kingpin.LongHelpTemplate)
stdout := colorable.NewColorableStdout()
result := lib.SimpleResult{}
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
case dump.FullCommand(): // Dump certificate
files := inputFiles(*dumpFiles)
defer func() {
for _, file := range files {
file.Close()
}
}()
var err error
if *dumpPem {
err = lib.ReadAsPEMFromFiles(files, *dumpType, readPassword, func(block *pem.Block) {
block.Headers = nil
pem.Encode(os.Stdout, block)
})
} else {
err = lib.ReadAsX509FromFiles(files, *dumpType, readPassword, func(cert *x509.Certificate, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing block: %s\n", strings.TrimSuffix(err.Error(), "\n"))
} else {
result.Certificates = append(result.Certificates, cert)
}
})
if *dumpJSON {
blob, _ := json.Marshal(result)
fmt.Println(string(blob))
} else {
for i, cert := range result.Certificates {
fmt.Fprintf(stdout, "** CERTIFICATE %d **\n", i+1)
fmt.Fprintf(stdout, "%s\n\n", lib.EncodeX509ToText(cert, terminalWidth, *verbose))
}
}
}
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", strings.TrimSuffix(err.Error(), "\n"))
os.Exit(1)
} else if len(result.Certificates) == 0 && !*dumpPem {
fmt.Fprintf(os.Stderr, "warning: no certificates found in input\n")
}
case connect.FullCommand(): // Get certs by connecting to a server
if connectStartTLS == nil && connectIdentity != nil {
fmt.Fprintln(os.Stderr, "error: --identity can only be used with --start-tls")
os.Exit(1)
}
connState, cri, err := starttls.GetConnectionState(
*connectStartTLS, *connectName, *connectTo, *connectIdentity,
*connectCert, *connectKey, *connectProxy, *connectTimeout)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSuffix(err.Error(), "\n"))
os.Exit(1)
}
result.TLSConnectionState = connState
result.CertificateRequestInfo = cri
for _, cert := range connState.PeerCertificates {
if *connectPem {
pem.Encode(os.Stdout, lib.EncodeX509ToPEM(cert, nil))
} else {
result.Certificates = append(result.Certificates, cert)
}
}
var hostname string
if *connectName != "" {
hostname = *connectName
} else {
hostname = strings.Split(*connectTo, ":")[0]
}
verifyResult := lib.VerifyChain(connState.PeerCertificates, connState.OCSPResponse, hostname, *connectCaPath)
result.VerifyResult = &verifyResult
if *connectJSON {
blob, _ := json.Marshal(result)
fmt.Println(string(blob))
} else if !*connectPem {
fmt.Fprintf(
stdout, "%s\n\n",
lib.EncodeTLSInfoToText(result.TLSConnectionState, result.CertificateRequestInfo))
for i, cert := range result.Certificates {
fmt.Fprintf(stdout, "** CERTIFICATE %d **\n", i+1)
fmt.Fprintf(stdout, "%s\n\n", lib.EncodeX509ToText(cert, terminalWidth, *verbose))
}
lib.PrintVerifyResult(stdout, *result.VerifyResult)
}
if *connectVerify && len(result.VerifyResult.Error) > 0 {
os.Exit(1)
}
case verify.FullCommand():
file := inputFile(*verifyFile)
defer file.Close()
chain := []*x509.Certificate{}
lib.ReadAsX509FromFiles([]*os.File{file}, *verifyType, readPassword, func(cert *x509.Certificate, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing block: %s\n", strings.TrimSuffix(err.Error(), "\n"))
} else {
chain = append(chain, cert)
}
})
verifyResult := lib.VerifyChain(chain, nil, *verifyName, *verifyCaPath)
if *verifyJSON {
blob, _ := json.Marshal(verifyResult)
fmt.Println(string(blob))
} else {
lib.PrintVerifyResult(stdout, verifyResult)
}
if verifyResult.Error != "" {
os.Exit(1)
}
}
}
func inputFile(fileName string) *os.File {
if fileName == "" {
return os.Stdin
}
rawFile, err := os.Open(fileName)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to open file: %s\n", err)
os.Exit(1)
}
return rawFile
}
func inputFiles(fileNames []string) []*os.File {
files := []*os.File{}
if fileNames != nil {
for _, filename := range fileNames {
rawFile, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to open file: %s\n", err)
os.Exit(1)
}
files = append(files, rawFile)
}
} else {
files = append(files, os.Stdin)
}
return files
}
func determineTerminalWidth() (width int) {
fd := int(os.Stdout.Fd())
if terminal.IsTerminal(fd) {
var err error
width, _, err = terminal.GetSize(fd)
if err != nil {
width = minWidth
}
} else {
width = minWidth
}
if width > maxWidth {
width = maxWidth
} else if width < minWidth {
width = minWidth
}
return
}
func readPassword(alias string) string {
if *dumpPassword != "" {
return *dumpPassword
}
if *verifyPassword != "" {
return *verifyPassword
}
var tty *os.File
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
tty = os.Stdin
} else {
defer tty.Close()
}
tty.WriteString("Enter password")
if alias != "" {
tty.WriteString(fmt.Sprintf(" for entry [%s]", alias))
}
tty.WriteString(": ")
password, err := terminal.ReadPassword(int(tty.Fd()))
tty.WriteString("\n")
if err != nil {
fmt.Fprintf(os.Stderr, "error reading password: %s\n", err)
os.Exit(1)
}
return strings.TrimSuffix(string(password), "\n")
os.Exit(cli.Run(os.Args[1:], terminal.OpenTTY()))
}
+1 -1
View File
@@ -33,7 +33,7 @@ import (
)
// Protocols are the names of supported protocols
var Protocols []string = []string{"mysql", "postgres", "psql", "smtp", "ldap", "ftp", "imap"}
var Protocols = []string{"mysql", "postgres", "psql", "smtp", "ldap", "ftp", "imap"}
type connectResult struct {
state *tls.ConnectionState