From d9d52c571254b06ff9cc61238ec45131dee5a913 Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Thu, 6 Oct 2016 16:44:59 -0700 Subject: [PATCH 1/2] Factor out useful functions into library --- lib/certs.go | 330 +++++++++++++++++++++++++++++++++++ display.go => lib/display.go | 54 ++++-- encoder.go => lib/encoder.go | 96 +++++----- main.go | 320 +++------------------------------ verify.go | 82 +++++++-- 5 files changed, 520 insertions(+), 362 deletions(-) create mode 100644 lib/certs.go rename display.go => lib/display.go (84%) rename encoder.go => lib/encoder.go (79%) diff --git a/lib/certs.go b/lib/certs.go new file mode 100644 index 0000000..cece9b8 --- /dev/null +++ b/lib/certs.go @@ -0,0 +1,330 @@ +/*- + * Copyright 2016 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lib + +import ( + "bufio" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/binary" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/square/certigo/jceks" + "github.com/square/certigo/pkcs7" + "golang.org/x/crypto/pkcs12" +) + +const ( + // nameHeader is the PEM header field for the friendly name/alias of the key in the key store. + nameHeader = "friendlyName" + + // fileHeader is the origin file where the key came from (as in file on disk). + fileHeader = "originFile" +) + +var fileExtToFormat = map[string]string{ + ".pem": "PEM", + ".crt": "PEM", + ".p7b": "PEM", + ".p7c": "PEM", + ".p12": "PKCS12", + ".pfx": "PKCS12", + ".jceks": "JCEKS", + ".jks": "JCEKS", // Only partially supported + ".der": "DER", +} + +var badSignatureAlgorithms = [...]x509.SignatureAlgorithm{ + x509.MD2WithRSA, + x509.MD5WithRSA, + x509.SHA1WithRSA, + x509.DSAWithSHA1, + x509.ECDSAWithSHA1, +} + +// ReadPEMFromFiles will read PEM blocks from the given set of inputs. +func ReadPEMFromFiles(files []*os.File, format string, password func(string) string, callback func(*pem.Block)) error { + for _, file := range files { + reader := bufio.NewReaderSize(file, 4) + format, err := formatForFile(reader, file.Name(), format) + if err != nil { + return fmt.Errorf("unable to guess file type (for file %s)\n", file.Name()) + } + + readCertsFromStream(reader, file.Name(), format, password, callback) + } + return nil +} + +// ReadPEM will read PEM blocks from the given set of inputs. +func ReadPEM(readers []io.Reader, format string, password func(string) string, callback func(*pem.Block)) error { + for _, r := range readers { + reader := bufio.NewReaderSize(r, 4) + format, err := formatForFile(reader, "", format) + if err != nil { + return fmt.Errorf("unable to guess format for input stream") + } + + readCertsFromStream(reader, "", format, password, callback) + } + return nil +} + +// ReadX509FromFiles will read X.509 certificates from the given set of inputs. +func ReadX509FromFiles(files []*os.File, format string, password func(string) string, callback func(*x509.Certificate)) error { + for _, file := range files { + reader := bufio.NewReaderSize(file, 4) + format, err := formatForFile(reader, file.Name(), format) + if err != nil { + return fmt.Errorf("unable to guess file type (for file %s)", file.Name()) + } + + readCertsFromStream(reader, file.Name(), format, password, pemToX509(callback)) + } + + return nil +} + +// ReadX509 will read X.509 certificates from the given set of inputs. +func ReadX509(readers []io.Reader, format string, password func(string) string, callback func(*x509.Certificate)) error { + for _, r := range readers { + reader := bufio.NewReaderSize(r, 4) + format, err := formatForFile(reader, "", format) + if err != nil { + return fmt.Errorf("unable to guess format for input stream") + } + + readCertsFromStream(reader, "", format, password, pemToX509(callback)) + } + + return nil +} + +func pemToX509(callback func(*x509.Certificate)) func(*pem.Block) { + return func(block *pem.Block) { + switch block.Type { + case "CERTIFICATE": + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + callback(cert) + } + case "PKCS7": + certs, err := pkcs7.ExtractCertificates(block.Bytes) + if err == nil { + for _, cert := range certs { + callback(cert) + } + } + } + } +} + +// 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 { + headers := map[string]string{} + if filename != "" && filename != os.Stdin.Name() { + headers[fileHeader] = filename + } + + switch format { + case "PEM": + scanner := pemScanner(reader) + for scanner.Scan() { + block, _ := pem.Decode(scanner.Bytes()) + block.Headers = mergeHeaders(block.Headers, headers) + callback(block) + } + case "DER": + data, err := ioutil.ReadAll(reader) + if err != nil { + return fmt.Errorf("error reading input: %s\n", err) + } + x509Certs, err := x509.ParseCertificates(data) + if err == nil { + for _, cert := range x509Certs { + callback(EncodeX509ToPEM(cert, headers)) + } + return nil + } + p7bBlocks, err := pkcs7.ParseSignedData(data) + if err == nil { + for _, block := range p7bBlocks { + callback(pkcs7ToPem(block, headers)) + } + return nil + } + return fmt.Errorf("error parsing certificates from DER data\n") + case "PKCS12": + data, err := ioutil.ReadAll(reader) + if err != nil { + return fmt.Errorf("error reading input: %s\n", err) + } + blocks, err := pkcs12.ToPEM(data, password("")) + if err != nil || len(blocks) == 0 { + fmt.Fprint(os.Stderr, "keystore appears to be empty or password was incorrect\n") + } + for _, block := range blocks { + block.Headers = mergeHeaders(block.Headers, headers) + callback(block) + } + case "JCEKS": + keyStore, err := jceks.LoadFromReader(reader, []byte(password(""))) + if err != nil { + return fmt.Errorf("error parsing keystore: %s\n", err) + } + for _, alias := range keyStore.ListCerts() { + cert, _ := keyStore.GetCert(alias) + callback(EncodeX509ToPEM(cert, mergeHeaders(headers, map[string]string{nameHeader: alias}))) + } + for _, alias := range keyStore.ListPrivateKeys() { + key, certs, err := keyStore.GetPrivateKeyAndCerts(alias, []byte(password(alias))) + if err != nil { + return fmt.Errorf("error parsing keystore: %s\n", err) + } + block, err := keyToPem(key, mergeHeaders(headers, map[string]string{nameHeader: alias})) + if err != nil { + return fmt.Errorf("error reading key: %s\n", err) + } + callback(block) + for _, cert := range certs { + callback(EncodeX509ToPEM(cert, mergeHeaders(headers, map[string]string{nameHeader: alias}))) + } + } + } + return fmt.Errorf("unknown file type: %s\n", format) +} + +func mergeHeaders(baseHeaders, extraHeaders map[string]string) (headers map[string]string) { + headers = map[string]string{} + for k, v := range baseHeaders { + headers[k] = v + } + for k, v := range extraHeaders { + headers[k] = v + } + return +} + +// EncodeX509ToPEM converts an X.509 certificate into a PEM block for output. +func EncodeX509ToPEM(cert *x509.Certificate, headers map[string]string) *pem.Block { + return &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + Headers: headers, + } +} + +// Convert a PKCS7 envelope into a PEM block for output. +func pkcs7ToPem(block *pkcs7.SignedDataEnvelope, headers map[string]string) *pem.Block { + return &pem.Block{ + Type: "PKCS7", + Bytes: block.Raw, + Headers: headers, + } +} + +// Convert a key into one or more PEM blocks for output. +func keyToPem(key crypto.PrivateKey, headers map[string]string) (*pem.Block, error) { + switch k := key.(type) { + case *rsa.PrivateKey: + return &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + Headers: headers, + }, nil + case *ecdsa.PrivateKey: + raw, err := x509.MarshalECPrivateKey(k) + if err != nil { + return nil, fmt.Errorf("error marshaling key: %s\n", reflect.TypeOf(key)) + } + return &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: raw, + Headers: headers, + }, nil + } + return nil, fmt.Errorf("unknown key type: %s\n", reflect.TypeOf(key)) +} + +// formatForFile returns the file format (either from flags or +// based on file extension). +func formatForFile(file *bufio.Reader, filename, format string) (string, error) { + // First, honor --format flag we got from user + if format != "" { + return format, nil + } + + // Second, attempt to guess based on extension + guess, ok := fileExtToFormat[strings.ToLower(filepath.Ext(filename))] + if ok { + return guess, nil + } + + // Third, attempt to guess based on first 4 bytes of input + data, err := file.Peek(4) + if err != nil { + return "", fmt.Errorf("unable to read file: %s\n", err) + } + + // Heuristics for guessing -- best effort. + magic := binary.BigEndian.Uint32(data) + if magic == 0xCECECECE || magic == 0xFEEDFEED { + // JCEKS/JKS files always start with this prefix + return "JCEKS", nil + } + if magic == 0x2D2D2D2D || magic == 0x434f4e4e { + // Starts with '----' or 'CONN' (what s_client prints...) + return "PEM", nil + } + if magic&0xFFFF0000 == 0x30820000 { + // Looks like the input is DER-encoded, so it's either PKCS12 or X.509. + if magic&0x0000FF00 == 0x0300 { + // Probably X.509 + return "DER", nil + } + return "PKCS12", nil + } + + return "", fmt.Errorf("unable to guess file format") +} + +// pemScanner will return a bufio.Scanner that splits the input +// from the given reader into PEM blocks. +func pemScanner(reader io.Reader) *bufio.Scanner { + scanner := bufio.NewScanner(reader) + + scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) { + block, rest := pem.Decode(data) + if block != nil { + size := len(data) - len(rest) + return size, data[:size], nil + } + + return 0, nil, nil + }) + + return scanner +} diff --git a/display.go b/lib/display.go similarity index 84% rename from display.go rename to lib/display.go index dfcaa12..e2b0451 100644 --- a/display.go +++ b/lib/display.go @@ -14,13 +14,15 @@ * limitations under the License. */ -package main +package lib import ( + "bufio" + "bytes" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" - "os" "text/template" "time" @@ -68,11 +70,15 @@ type certWithName struct { cert *x509.Certificate } -func createSimpleCertificateFromX509(block *pem.Block) simpleCertificate { +func (c certWithName) MarshalJSON() ([]byte, error) { + out := createSimpleCertificate(c.name, c.cert) + return json.Marshal(out) +} + +func createSimpleCertificateFromX509(block *pem.Block) (simpleCertificate, error) { raw, err := x509.ParseCertificate(block.Bytes) if err != nil { - fmt.Fprintf(os.Stderr, "error reading cert: %s", err) - os.Exit(1) + return simpleCertificate{}, fmt.Errorf("error reading cert: %s", err) } cert := certWithName{cert: raw} @@ -83,14 +89,34 @@ func createSimpleCertificateFromX509(block *pem.Block) simpleCertificate { cert.file = val } - return createSimpleCertificate(cert) + return createSimpleCertificate(cert.name, cert.cert), nil +} + +// EncodeX509ToJSON encodes an X.509 certificate into a JSON string. +func EncodeX509ToJSON(cert *x509.Certificate) []byte { + out := createSimpleCertificate("", cert) + raw, err := json.Marshal(out) + if err != nil { + panic(err) + } + return raw +} + +// EncodeX509ToObject encodes an X.509 certificate into a JSON-serializable object. +func EncodeX509ToObject(cert *x509.Certificate) interface{} { + return createSimpleCertificate("", cert) +} + +// EncodeX509ToText encodes an X.509 certificate into human-readable text. +func EncodeX509ToText(cert *x509.Certificate) []byte { + return displayCert(createSimpleCertificate("", cert)) } // displayCert takes in a parsed certificate object // (for jceks certs, blank otherwise), and prints out relevant // information. Start and end dates are colored based on whether or not // the certificate is expired, not expired, or close to expiring. -func displayCert(cert simpleCertificate) { +func displayCert(cert simpleCertificate) []byte { funcMap := template.FuncMap{ "certStart": certStart, "certEnd": certEnd, @@ -106,11 +132,15 @@ func displayCert(cert simpleCertificate) { // Should never happen panic(err) } - err = t.Execute(os.Stdout, cert) + var buffer bytes.Buffer + w := bufio.NewWriter(&buffer) + err = t.Execute(w, cert) if err != nil { // Should never happen panic(err) } + w.Flush() + return buffer.Bytes() } var ( @@ -185,14 +215,6 @@ func certEnd(end time.Time) string { } } -var badSignatureAlgorithms = []x509.SignatureAlgorithm{ - x509.MD2WithRSA, - x509.MD5WithRSA, - x509.SHA1WithRSA, - x509.DSAWithSHA1, - x509.ECDSAWithSHA1, -} - func redify(text string) string { return red.SprintfFunc()("%s", text) } diff --git a/encoder.go b/lib/encoder.go similarity index 79% rename from encoder.go rename to lib/encoder.go index 56e0590..75921ba 100644 --- a/encoder.go +++ b/lib/encoder.go @@ -1,4 +1,20 @@ -package main +/*- + * Copyright 2016 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lib import ( "bytes" @@ -81,6 +97,7 @@ type nameConstraints struct { PermittedDNSDomains []string `json:"permitted_dns_domains,omitempty"` } +// simpleCertificate is a JSON-representable certificate metadata holder. type simpleCertificate struct { Alias string `json:"alias,omitempty"` SerialNumber string `json:"serial"` @@ -88,8 +105,8 @@ type simpleCertificate struct { NotAfter time.Time `json:"not_after"` SignatureAlgorithm simpleSigAlg `json:"signature_algorithm"` IsSelfSigned bool `json:"is_self_signed"` - Subject simplePkixName `json:"subject"` - Issuer simplePkixName `json:"issuer"` + Subject simplePKIXName `json:"subject"` + Issuer simplePKIXName `json:"issuer"` BasicConstraints *basicConstraints `json:"basic_constraints,omitempty"` NameConstraints *nameConstraints `json:"name_constraints,omitempty"` KeyUsage simpleKeyUsage `json:"key_usage,omitempty"` @@ -101,7 +118,7 @@ type simpleCertificate struct { PEM string `json:"pem,omitempty"` } -type simplePkixName struct { +type simplePKIXName struct { Name pkix.Name KeyID []byte } @@ -111,53 +128,48 @@ type simpleExtKeyUsage x509.ExtKeyUsage type simpleSigAlg x509.SignatureAlgorithm -type simpleResult struct { - Certificates []simpleCertificate `json:"certificates"` - VerifyResult *simpleVerification `json:"verify_result,omitempty"` -} - -func createSimpleCertificate(c certWithName) simpleCertificate { +func createSimpleCertificate(name string, cert *x509.Certificate) simpleCertificate { out := simpleCertificate{ - Alias: c.name, - SerialNumber: c.cert.SerialNumber.String(), - NotBefore: c.cert.NotBefore, - NotAfter: c.cert.NotAfter, - SignatureAlgorithm: simpleSigAlg(c.cert.SignatureAlgorithm), - IsSelfSigned: isSelfSigned(c.cert), - Subject: simplePkixName{ - Name: c.cert.Subject, - KeyID: c.cert.SubjectKeyId, + Alias: name, + SerialNumber: cert.SerialNumber.String(), + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + SignatureAlgorithm: simpleSigAlg(cert.SignatureAlgorithm), + IsSelfSigned: IsSelfSigned(cert), + Subject: simplePKIXName{ + Name: cert.Subject, + KeyID: cert.SubjectKeyId, }, - Issuer: simplePkixName{ - Name: c.cert.Issuer, - KeyID: c.cert.AuthorityKeyId, + Issuer: simplePKIXName{ + Name: cert.Issuer, + KeyID: cert.AuthorityKeyId, }, - KeyUsage: simpleKeyUsage(c.cert.KeyUsage), - AltDNSNames: c.cert.DNSNames, - AltIPAddresses: c.cert.IPAddresses, - EmailAddresses: c.cert.EmailAddresses, - Warnings: certWarnings(c.cert), - PEM: string(pem.EncodeToMemory(certToPem(c.cert, nil))), + KeyUsage: simpleKeyUsage(cert.KeyUsage), + AltDNSNames: cert.DNSNames, + AltIPAddresses: cert.IPAddresses, + EmailAddresses: cert.EmailAddresses, + Warnings: certWarnings(cert), + PEM: string(pem.EncodeToMemory(EncodeX509ToPEM(cert, nil))), } - if c.cert.BasicConstraintsValid { + if cert.BasicConstraintsValid { out.BasicConstraints = &basicConstraints{ - IsCA: c.cert.IsCA, + IsCA: cert.IsCA, } - if c.cert.MaxPathLen > 0 || c.cert.MaxPathLenZero { - out.BasicConstraints.MaxPathLen = &c.cert.MaxPathLen + if cert.MaxPathLen > 0 || cert.MaxPathLenZero { + out.BasicConstraints.MaxPathLen = &cert.MaxPathLen } } - if len(c.cert.PermittedDNSDomains) > 0 { + if len(cert.PermittedDNSDomains) > 0 { out.NameConstraints = &nameConstraints{ - Critical: c.cert.PermittedDNSDomainsCritical, - PermittedDNSDomains: c.cert.PermittedDNSDomains, + Critical: cert.PermittedDNSDomainsCritical, + PermittedDNSDomains: cert.PermittedDNSDomains, } } simpleEku := []simpleExtKeyUsage{} - for _, eku := range c.cert.ExtKeyUsage { + for _, eku := range cert.ExtKeyUsage { simpleEku = append(simpleEku, simpleExtKeyUsage(eku)) } out.ExtKeyUsage = simpleEku @@ -165,12 +177,7 @@ func createSimpleCertificate(c certWithName) simpleCertificate { return out } -func (c certWithName) MarshalJSON() ([]byte, error) { - out := createSimpleCertificate(c) - return json.Marshal(out) -} - -func (p simplePkixName) MarshalJSON() ([]byte, error) { +func (p simplePKIXName) MarshalJSON() ([]byte, error) { out := map[string]interface{}{} if p.Name.CommonName != "" { @@ -322,3 +329,8 @@ func algWarnings(cert *x509.Certificate) (warnings []string) { return } + +// IsSelfSigned returns true iff the given certificate has a valid self-signature. +func IsSelfSigned(cert *x509.Certificate) bool { + return cert.CheckSignatureFrom(cert) == nil +} diff --git a/main.go b/main.go index 077563b..9b36439 100644 --- a/main.go +++ b/main.go @@ -17,28 +17,17 @@ package main import ( - "bufio" - "crypto" - "crypto/ecdsa" - "crypto/rsa" "crypto/tls" "crypto/x509" - "encoding/binary" "encoding/json" "encoding/pem" "fmt" - "io" - "io/ioutil" "os" - "path/filepath" - "reflect" "strings" "gopkg.in/alecthomas/kingpin.v2" - "github.com/square/certigo/jceks" - "github.com/square/certigo/pkcs7" - "golang.org/x/crypto/pkcs12" + "github.com/square/certigo/lib" "golang.org/x/crypto/ssh/terminal" ) @@ -63,26 +52,10 @@ var ( verifyFile = verify.Arg("file", "Certificate file to dump (or stdin if not specified).").ExistingFile() verifyName = verify.Flag("name", "Server name to verify certificate against.").Required().String() verifyCaPath = verify.Flag("ca", "Path to CA bundle (system default if unspecified).").ExistingFile() + verifyType = verify.Flag("format", "Format of given input (PEM, DER, JCEKS, PKCS12; heuristic if missing).").String() verifyJSON = verify.Flag("json", "Write output as machine-readable JSON format.").Bool() ) -const ( - nameHeader = "friendlyName" - fileHeader = "originFile" -) - -var fileExtToFormat = map[string]string{ - ".pem": "PEM", - ".crt": "PEM", - ".p7b": "PEM", - ".p7c": "PEM", - ".p12": "PKCS12", - ".pfx": "PKCS12", - ".jceks": "JCEKS", - ".jks": "JCEKS", // Only partially supported - ".der": "DER", -} - func main() { app.Version("1.4.0") @@ -96,36 +69,24 @@ func main() { } }() - readCerts(files, func(block *pem.Block) { - if *dumpPem { + if *dumpPem { + lib.ReadPEMFromFiles(files, *dumpType, readPassword, func(block *pem.Block) { block.Headers = nil pem.Encode(os.Stdout, block) - return - } - - switch block.Type { - case "CERTIFICATE": - result.Certificates = append(result.Certificates, createSimpleCertificateFromX509(block)) - case "PKCS7": - certs, err := pkcs7.ExtractCertificates(block.Bytes) - if err != nil { - fmt.Fprintf(os.Stderr, "error parsing PKCS7 block: %s\n", err) - os.Exit(1) - } - for _, cert := range certs { - result.Certificates = append(result.Certificates, createSimpleCertificate(certWithName{cert: cert})) - } - } - }) - - if *dumpJSON { - blob, _ := json.Marshal(result) - fmt.Println(string(blob)) + }) } else { - for i, cert := range result.Certificates { - fmt.Printf("** CERTIFICATE %d **\n", i+1) - displayCert(cert) - fmt.Printf("\n\n") + lib.ReadX509FromFiles(files, *dumpType, readPassword, func(cert *x509.Certificate) { + result.Certificates = append(result.Certificates, cert) + }) + + if *dumpJSON { + blob, _ := json.Marshal(result) + fmt.Println(string(blob)) + } else { + for i, cert := range result.Certificates { + fmt.Printf("** CERTIFICATE %d **\n", i+1) + fmt.Printf("%s\n\n", lib.EncodeX509ToText(cert)) + } } } @@ -142,9 +103,9 @@ func main() { defer conn.Close() for _, cert := range conn.ConnectionState().PeerCertificates { if *connectPem { - pem.Encode(os.Stdout, certToPem(cert, nil)) + pem.Encode(os.Stdout, lib.EncodeX509ToPEM(cert, nil)) } else { - result.Certificates = append(result.Certificates, createSimpleCertificate(certWithName{cert: cert})) + result.Certificates = append(result.Certificates, cert) } } @@ -165,8 +126,7 @@ func main() { } else if !*connectPem { for i, cert := range result.Certificates { fmt.Printf("** CERTIFICATE %d **\n", i+1) - displayCert(cert) - fmt.Print("\n\n") + fmt.Printf("%s\n\n", lib.EncodeX509ToText(cert)) } printVerifyResult(*result.VerifyResult) } @@ -175,23 +135,8 @@ func main() { defer file.Close() chain := []*x509.Certificate{} - readCerts([]*os.File{file}, func(block *pem.Block) { - switch block.Type { - case "CERTIFICATE": - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading cert: %s", err) - os.Exit(1) - } - chain = append(chain, cert) - case "PKCS7": - certs, err := pkcs7.ExtractCertificates(block.Bytes) - if err != nil { - fmt.Fprintf(os.Stderr, "error parsing PKCS7 block: %s\n", err) - os.Exit(1) - } - chain = append(chain, certs...) - } + lib.ReadX509FromFiles([]*os.File{file}, *verifyType, readPassword, func(cert *x509.Certificate) { + chain = append(chain, cert) }) verifyResult := verifyChain(chain, *verifyName, *verifyCaPath) @@ -237,20 +182,7 @@ func inputFiles(fileNames []string) []*os.File { return files } -func readCerts(files []*os.File, callback func(*pem.Block)) { - for _, file := range files { - reader := bufio.NewReaderSize(file, 4) - format, ok := formatForFile(reader, file.Name(), *dumpType) - if !ok { - fmt.Fprintf(os.Stderr, "unable to guess file type (for file %s)\n", file.Name()) - os.Exit(1) - } - - readCertsFromFile(reader, file.Name(), format, callback) - } -} - -func readPassword(prompt string) string { +func readPassword(alias string) string { if *dumpPassword != "" { return *dumpPassword } @@ -263,7 +195,12 @@ func readPassword(prompt string) string { defer tty.Close() } - tty.WriteString(prompt) + 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 { @@ -273,202 +210,3 @@ func readPassword(prompt string) string { return strings.TrimSuffix(string(password), "\n") } - -// formatForFile returns the file format (either from flags or -// based on file extension). -func formatForFile(file *bufio.Reader, filename, format string) (string, bool) { - // First, honor --format flag we got from user - if format != "" { - return format, true - } - - // Second, attempt to guess based on extension - guess, ok := fileExtToFormat[strings.ToLower(filepath.Ext(filename))] - if ok { - return guess, true - } - - // Third, attempt to guess based on first 4 bytes of input - data, err := file.Peek(4) - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(1) - } - - // Heuristics for guessing -- best effort. - magic := binary.BigEndian.Uint32(data) - if magic == 0xCECECECE || magic == 0xFEEDFEED { - // JCEKS/JKS files always start with this prefix - return "JCEKS", true - } - if magic == 0x2D2D2D2D || magic == 0x434f4e4e { - // Starts with '----' or 'CONN' (what s_client prints...) - return "PEM", true - } - if magic&0xFFFF0000 == 0x30820000 { - // Looks like the input is DER-encoded, so it's either PKCS12 or X.509. - if magic&0x0000FF00 == 0x0300 { - // Probably X.509 - return "DER", true - } - return "PKCS12", true - } - - return "", false -} - -// pemScanner will return a bufio.Scanner that splits the input -// from the given reader into PEM blocks. -func pemScanner(reader io.Reader) *bufio.Scanner { - scanner := bufio.NewScanner(reader) - - scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) { - block, rest := pem.Decode(data) - if block != nil { - size := len(data) - len(rest) - return size, data[:size], nil - } - - return 0, nil, nil - }) - - return scanner -} - -// readCertsFromFile takes some input and converts it to PEM blocks. -func readCertsFromFile(reader io.Reader, filename string, format string, callback func(*pem.Block)) { - headers := map[string]string{} - if filename != "" && filename != os.Stdin.Name() { - headers[fileHeader] = filename - } - - switch format { - case "PEM": - scanner := pemScanner(reader) - for scanner.Scan() { - block, _ := pem.Decode(scanner.Bytes()) - block.Headers = mergeHeaders(block.Headers, headers) - callback(block) - } - case "DER": - data, err := ioutil.ReadAll(reader) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading input: %s\n", err) - os.Exit(1) - } - x509Certs, err := x509.ParseCertificates(data) - if err == nil { - for _, cert := range x509Certs { - callback(certToPem(cert, headers)) - } - return - } - p7bBlocks, err := pkcs7.ParseSignedData(data) - if err == nil { - for _, block := range p7bBlocks { - callback(pkcs7ToPem(block, headers)) - } - return - } - fmt.Fprintf(os.Stderr, "error parsing certificates from DER data\n") - os.Exit(1) - case "PKCS12": - data, err := ioutil.ReadAll(reader) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading input: %s\n", err) - os.Exit(1) - } - password := readPassword("Enter password: ") - blocks, err := pkcs12.ToPEM(data, password) - if err != nil || len(blocks) == 0 { - fmt.Fprint(os.Stderr, "keystore appears to be empty or password was incorrect\n") - os.Exit(1) - } - for _, block := range blocks { - block.Headers = mergeHeaders(block.Headers, headers) - callback(block) - } - case "JCEKS": - password := readPassword("Enter password: ") - keyStore, err := jceks.LoadFromReader(reader, []byte(password)) - if err != nil { - fmt.Fprintf(os.Stderr, "error parsing keystore: %s\n", err) - os.Exit(1) - } - for _, alias := range keyStore.ListCerts() { - cert, _ := keyStore.GetCert(alias) - callback(certToPem(cert, mergeHeaders(headers, map[string]string{nameHeader: alias}))) - } - for _, alias := range keyStore.ListPrivateKeys() { - password := readPassword(fmt.Sprintf("Enter password for alias [%s]: ", alias)) - key, certs, err := keyStore.GetPrivateKeyAndCerts(alias, []byte(password)) - if err != nil { - fmt.Fprintf(os.Stderr, "error parsing keystore: %s\n", err) - os.Exit(1) - } - callback(keyToPem(key, mergeHeaders(headers, map[string]string{nameHeader: alias}))) - for _, cert := range certs { - callback(certToPem(cert, mergeHeaders(headers, map[string]string{nameHeader: alias}))) - } - } - default: - fmt.Fprintf(os.Stderr, "unknown file type: %s\n", format) - os.Exit(1) - } -} - -func mergeHeaders(baseHeaders, extraHeaders map[string]string) (headers map[string]string) { - headers = map[string]string{} - for k, v := range baseHeaders { - headers[k] = v - } - for k, v := range extraHeaders { - headers[k] = v - } - return -} - -// Convert an X.509 cert into a PEM block for output. -func certToPem(cert *x509.Certificate, headers map[string]string) *pem.Block { - return &pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - Headers: headers, - } -} - -// Convert a PKCS7 envelope into a PEM block for output. -func pkcs7ToPem(block *pkcs7.SignedDataEnvelope, headers map[string]string) *pem.Block { - return &pem.Block{ - Type: "PKCS7", - Bytes: block.Raw, - Headers: headers, - } -} - -// Convert a key into one or more PEM blocks for output. -func keyToPem(key crypto.PrivateKey, headers map[string]string) *pem.Block { - switch k := key.(type) { - case *rsa.PrivateKey: - return &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(k), - Headers: headers, - } - case *ecdsa.PrivateKey: - raw, err := x509.MarshalECPrivateKey(k) - if err != nil { - fmt.Fprintf(os.Stderr, "error marshaling key: %s\n", reflect.TypeOf(key)) - os.Exit(1) - } - return &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: raw, - Headers: headers, - } - default: - fmt.Fprintf(os.Stderr, "unknown key type: %s\n", reflect.TypeOf(key)) - os.Exit(1) - } - return nil -} diff --git a/verify.go b/verify.go index c1a8df9..6aa3025 100644 --- a/verify.go +++ b/verify.go @@ -18,17 +18,51 @@ package main import ( "crypto/x509" + "encoding/json" "encoding/pem" "fmt" "io/ioutil" "os" + "strconv" + + "github.com/fatih/color" + "github.com/square/certigo/lib" ) +var ( + green = color.New(color.Bold, color.FgGreen) + yellow = color.New(color.Bold, color.FgYellow) + red = color.New(color.Bold, color.FgRed) +) + +var algoName = [...]string{ + x509.MD2WithRSA: "MD2-RSA", + x509.MD5WithRSA: "MD5-RSA", + x509.SHA1WithRSA: "SHA1-RSA", + x509.SHA256WithRSA: "SHA256-RSA", + x509.SHA384WithRSA: "SHA384-RSA", + x509.SHA512WithRSA: "SHA512-RSA", + x509.DSAWithSHA1: "DSA-SHA1", + x509.DSAWithSHA256: "DSA-SHA256", + x509.ECDSAWithSHA1: "ECDSA-SHA1", + x509.ECDSAWithSHA256: "ECDSA-SHA256", + x509.ECDSAWithSHA384: "ECDSA-SHA384", + x509.ECDSAWithSHA512: "ECDSA-SHA512", +} + +var badSignatureAlgorithms = [...]x509.SignatureAlgorithm{ + x509.MD2WithRSA, + x509.MD5WithRSA, + x509.SHA1WithRSA, + x509.DSAWithSHA1, + x509.ECDSAWithSHA1, +} + type simpleVerifyCert struct { - Name string `json:"name"` - IsSelfSigned bool `json:"is_self_signed"` - SignatureAlgorithm simpleSigAlg `json:"signature_algorithm"` - PEM string `json:"pem"` + Name string `json:"name"` + IsSelfSigned bool `json:"is_self_signed"` + PEM string `json:"pem"` + signatureAlgorithm x509.SignatureAlgorithm } type simpleVerification struct { @@ -36,6 +70,25 @@ type simpleVerification struct { Chains [][]simpleVerifyCert `json:"chains"` } +type simpleResult struct { + Certificates []*x509.Certificate `json:"certificates"` + VerifyResult *simpleVerification `json:"verify_result,omitempty"` +} + +func (s simpleResult) MarshalJSON() ([]byte, error) { + certs := make([]interface{}, len(s.Certificates)) + for i, c := range s.Certificates { + certs[i] = lib.EncodeX509ToObject(c) + } + + out := map[string]interface{}{} + out["certificates"] = certs + if s.VerifyResult != nil { + out["verify_result"] = s.VerifyResult + } + return json.Marshal(out) +} + func caBundle(caPath string) *x509.CertPool { if caPath == "" { return nil @@ -74,14 +127,13 @@ func verifyChain(certs []*x509.Certificate, dnsName, caPath string) simpleVerifi return result } - //green.Printf("Server certificates appear to be valid (found %d chains):\n", len(chains)) for _, chain := range chains { aChain := []simpleVerifyCert{} for _, cert := range chain { aCert := simpleVerifyCert{ - IsSelfSigned: isSelfSigned(cert), - SignatureAlgorithm: simpleSigAlg(cert.SignatureAlgorithm), - PEM: string(pem.EncodeToMemory(certToPem(cert, nil))), + IsSelfSigned: lib.IsSelfSigned(cert), + signatureAlgorithm: cert.SignatureAlgorithm, + PEM: string(pem.EncodeToMemory(lib.EncodeX509ToPEM(cert, nil))), } if cert.Subject.CommonName != "" { @@ -89,6 +141,7 @@ func verifyChain(certs []*x509.Certificate, dnsName, caPath string) simpleVerifi } else { aCert.Name = fmt.Sprintf("Serial #%s", cert.SerialNumber.String()) } + aChain = append(aChain, aCert) } result.Chains = append(result.Chains, aChain) @@ -102,7 +155,7 @@ func fmtCert(cert simpleVerifyCert) string { name += green.SprintfFunc()(" [self-signed]") } for _, alg := range badSignatureAlgorithms { - if x509.SignatureAlgorithm(cert.SignatureAlgorithm) == alg { + if cert.signatureAlgorithm == alg { name += red.SprintfFunc()(" [%s]", algString(alg)) break } @@ -110,6 +163,13 @@ func fmtCert(cert simpleVerifyCert) string { return name } +func algString(algo x509.SignatureAlgorithm) string { + if 0 < algo && int(algo) < len(algoName) { + return algoName[algo] + } + return strconv.Itoa(int(algo)) +} + func printVerifyResult(result simpleVerification) { if result.Error != "" { red.Printf("Failed to verify certificate chain:\n") @@ -126,7 +186,3 @@ func printVerifyResult(result simpleVerification) { } } } - -func isSelfSigned(cert *x509.Certificate) bool { - return cert.CheckSignatureFrom(cert) == nil -} From 4704138725b601b0432d9af8992cafb5d246a4cd Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Fri, 7 Oct 2016 14:26:03 -0700 Subject: [PATCH 2/2] Better naming and doc comments --- lib/certs.go | 28 ++++++++++++++++++++-------- main.go | 6 +++--- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/certs.go b/lib/certs.go index cece9b8..c1146a4 100644 --- a/lib/certs.go +++ b/lib/certs.go @@ -65,8 +65,11 @@ var badSignatureAlgorithms = [...]x509.SignatureAlgorithm{ x509.ECDSAWithSHA1, } -// ReadPEMFromFiles will read PEM blocks from the given set of inputs. -func ReadPEMFromFiles(files []*os.File, format string, password func(string) string, callback func(*pem.Block)) error { +// ReadAsPEMFromFiles will read PEM blocks from the given set of 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 PEM +// blocks and passed to the callback. +func ReadAsPEMFromFiles(files []*os.File, format string, password func(string) string, callback func(*pem.Block)) error { for _, file := range files { reader := bufio.NewReaderSize(file, 4) format, err := formatForFile(reader, file.Name(), format) @@ -79,8 +82,11 @@ func ReadPEMFromFiles(files []*os.File, format string, password func(string) str return nil } -// ReadPEM will read PEM blocks from the given set of inputs. -func ReadPEM(readers []io.Reader, format string, password func(string) string, callback func(*pem.Block)) error { +// ReadAsPEM will read PEM blocks from the given set of 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 PEM blocks and +// passed to the callback. +func ReadAsPEM(readers []io.Reader, format string, password func(string) string, callback func(*pem.Block)) error { for _, r := range readers { reader := bufio.NewReaderSize(r, 4) format, err := formatForFile(reader, "", format) @@ -93,8 +99,11 @@ func ReadPEM(readers []io.Reader, format string, password func(string) string, c return nil } -// ReadX509FromFiles will read X.509 certificates from the given set of inputs. -func ReadX509FromFiles(files []*os.File, format string, password func(string) string, callback func(*x509.Certificate)) error { +// ReadAsX509FromFiles will read X.509 certificates from the given set of +// 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 { for _, file := range files { reader := bufio.NewReaderSize(file, 4) format, err := formatForFile(reader, file.Name(), format) @@ -108,8 +117,11 @@ func ReadX509FromFiles(files []*os.File, format string, password func(string) st return nil } -// ReadX509 will read X.509 certificates from the given set of inputs. -func ReadX509(readers []io.Reader, format string, password func(string) string, callback func(*x509.Certificate)) error { +// ReadAsX509 will read X.509 certificates from the given set of 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 ReadAsX509(readers []io.Reader, format string, password func(string) string, callback func(*x509.Certificate)) error { for _, r := range readers { reader := bufio.NewReaderSize(r, 4) format, err := formatForFile(reader, "", format) diff --git a/main.go b/main.go index 9b36439..3791a5e 100644 --- a/main.go +++ b/main.go @@ -70,12 +70,12 @@ func main() { }() if *dumpPem { - lib.ReadPEMFromFiles(files, *dumpType, readPassword, func(block *pem.Block) { + lib.ReadAsPEMFromFiles(files, *dumpType, readPassword, func(block *pem.Block) { block.Headers = nil pem.Encode(os.Stdout, block) }) } else { - lib.ReadX509FromFiles(files, *dumpType, readPassword, func(cert *x509.Certificate) { + lib.ReadAsX509FromFiles(files, *dumpType, readPassword, func(cert *x509.Certificate) { result.Certificates = append(result.Certificates, cert) }) @@ -135,7 +135,7 @@ func main() { defer file.Close() chain := []*x509.Certificate{} - lib.ReadX509FromFiles([]*os.File{file}, *verifyType, readPassword, func(cert *x509.Certificate) { + lib.ReadAsX509FromFiles([]*os.File{file}, *verifyType, readPassword, func(cert *x509.Certificate) { chain = append(chain, cert) })