Merge pull request #201 from square/refactor

refactor certigo package main
This commit is contained in:
Matthew McPherrin
2020-02-06 15:10:05 -08:00
committed by GitHub
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