597 lines
17 KiB
Go
597 lines
17 KiB
Go
/*-
|
|
* 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"
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/binary"
|
|
"encoding/pem"
|
|
"errors"
|
|
"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",
|
|
".cer": "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,
|
|
}
|
|
|
|
func errorFromErrors(errs []error) error {
|
|
if len(errs) == 0 {
|
|
return nil
|
|
}
|
|
if len(errs) == 1 {
|
|
return errs[0]
|
|
}
|
|
buffer := new(bytes.Buffer)
|
|
buffer.WriteString("encountered multiple errors:\n")
|
|
for _, err := range errs {
|
|
buffer.WriteString("* ")
|
|
buffer.WriteString(strings.TrimSuffix(err.Error(), "\n"))
|
|
buffer.WriteString("\n")
|
|
}
|
|
return errors.New(buffer.String())
|
|
}
|
|
|
|
func NewCertificateKeyFormat(fileFormat string) CertificateKeyFormat {
|
|
fileFormat = strings.ToUpper(fileFormat)
|
|
switch fileFormat {
|
|
case "":
|
|
return CertKeyFormatNONE
|
|
case "PEM":
|
|
return CertKeyFormatPEM
|
|
case "DER":
|
|
return CertKeyFormatDER
|
|
case "PKCS12", "P12":
|
|
return CertKeyFormatPKCS12
|
|
default:
|
|
return CertKeyFormatNONE
|
|
}
|
|
}
|
|
|
|
type CertificateKeyFormat string
|
|
|
|
const (
|
|
CertKeyFormatNONE CertificateKeyFormat = ""
|
|
// The file contains plain-text PEM data
|
|
CertKeyFormatPEM CertificateKeyFormat = "PEM"
|
|
// The file contains X.509 DER encoded data
|
|
CertKeyFormatDER CertificateKeyFormat = "DER"
|
|
// The file contains JCEKS keystores
|
|
CertKeyFormatJCEKS CertificateKeyFormat = "JCEKS"
|
|
// The file contains PFX data describing PKCS#12
|
|
CertKeyFormatPKCS12 CertificateKeyFormat = "PKCS12"
|
|
)
|
|
|
|
func (f *CertificateKeyFormat) Set(fileFormat string) {
|
|
*f = NewCertificateKeyFormat(fileFormat)
|
|
}
|
|
|
|
func (f CertificateKeyFormat) IsNone() bool {
|
|
return f == CertKeyFormatNONE
|
|
}
|
|
|
|
func (f *CertificateKeyFormat) SetPEM() {
|
|
*f = CertKeyFormatPEM
|
|
}
|
|
|
|
func (f CertificateKeyFormat) IsPEM() bool {
|
|
return f == CertKeyFormatPEM
|
|
}
|
|
|
|
func (f CertificateKeyFormat) IsDER() bool {
|
|
return f == CertKeyFormatDER
|
|
}
|
|
|
|
func (f CertificateKeyFormat) IsPKCS12() bool {
|
|
return f == CertKeyFormatPKCS12
|
|
}
|
|
|
|
// ClientTLSConfigV2 builds transport-layer config for a gRPC client using the
|
|
// given properties. Support certificate file both PEM and P12.
|
|
func ClientTLSConfigV2(insecureSkipVerify bool, cacertFile string, cacertFormat CertificateKeyFormat, clientCertFile string, certFormat CertificateKeyFormat, clientKeyFile string, keyFormat CertificateKeyFormat, clientPass string) (*tls.Config, error) {
|
|
var tlsConf tls.Config
|
|
|
|
if clientCertFile != "" {
|
|
// Load the client certificates
|
|
pemCertBytes, err := readAsPEMEx2(clientCertFile, string(certFormat), clientPass)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not load client cert: %v", err)
|
|
}
|
|
pemKeyBytes := pemCertBytes // allow clientCertFile include both certificate and key file (JCEKS/PKCS12/PEM)
|
|
|
|
// Load the client key
|
|
if clientKeyFile != "" {
|
|
pemBytes, err := readAsPEMEx2(clientKeyFile, string(keyFormat), clientPass)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not load client key: %v", err)
|
|
}
|
|
pemKeyBytes = pemBytes
|
|
}
|
|
|
|
// Load tls.Certificate
|
|
certificate, err := tls.X509KeyPair(pemCertBytes, pemKeyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not load client key pair: %v", err)
|
|
}
|
|
tlsConf.Certificates = []tls.Certificate{certificate}
|
|
}
|
|
|
|
if insecureSkipVerify {
|
|
tlsConf.InsecureSkipVerify = true
|
|
} else if cacertFile != "" {
|
|
// Create a certificate pool from the certificate authority
|
|
certPool := x509.NewCertPool()
|
|
pemCACertBytes, err := readAsPEMEx2(cacertFile, string(cacertFormat), "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not load cacert : %v", err)
|
|
}
|
|
|
|
// Append the certificates from the CA
|
|
if ok := certPool.AppendCertsFromPEM(pemCACertBytes); !ok {
|
|
return nil, errors.New("failed to append ca certs")
|
|
}
|
|
|
|
tlsConf.RootCAs = certPool
|
|
}
|
|
|
|
return &tlsConf, nil
|
|
}
|
|
|
|
func GuessFormatForFile(filename, format string) (string, error) {
|
|
// Second, attempt to guess based on extension
|
|
guess, ok := fileExtToFormat[strings.ToLower(filepath.Ext(filename))]
|
|
if ok {
|
|
return guess, nil
|
|
}
|
|
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to open file: %s\n", err)
|
|
}
|
|
defer file.Close()
|
|
reader := bufio.NewReaderSize(file, 4)
|
|
|
|
// Third, attempt to guess based on first 4 bytes of input
|
|
data, err := reader.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...)
|
|
// TODO start with 'Certificate'
|
|
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 "", nil
|
|
}
|
|
|
|
func readAsPEMEx2(filename string, format string, password string) ([]byte, error) {
|
|
var pembuf bytes.Buffer
|
|
err := readAsPEMEx(filename, format, "", func(block *pem.Block, format string) error {
|
|
return pem.Encode(&pembuf, block)
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not load client cert: %v", err)
|
|
}
|
|
return pembuf.Bytes(), nil
|
|
}
|
|
|
|
func readAsPEMEx(filename string, format string, password string, callback func(*pem.Block, string) error) error {
|
|
rawFile, err := os.Open(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to open file: %s\n", err)
|
|
}
|
|
defer rawFile.Close()
|
|
passwordFunc := func(promet string) string {
|
|
return password
|
|
}
|
|
|
|
reader := bufio.NewReaderSize(rawFile, 4)
|
|
format, err = formatForFile(reader, "", format)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to guess format for input stream")
|
|
}
|
|
|
|
return readCertsFromStream(reader, "", format, passwordFunc, callback)
|
|
}
|
|
|
|
// // 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, string) error) error {
|
|
// var errs []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())
|
|
// }
|
|
//
|
|
// err = readCertsFromStream(reader, file.Name(), format, password, callback)
|
|
// if err != nil {
|
|
// errs = append(errs, err)
|
|
// }
|
|
// }
|
|
// return errorFromErrors(errs)
|
|
// }
|
|
//
|
|
|
|
// 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, string) error) error {
|
|
errs := []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")
|
|
}
|
|
|
|
err = readCertsFromStream(reader, "", format, password, callback)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return errorFromErrors(errs)
|
|
}
|
|
|
|
//// 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, string, error) error) error {
|
|
// errs := []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, try adding --format flag", file.Name())
|
|
// }
|
|
//
|
|
// err = readCertsFromStream(reader, file.Name(), format, password, pemToX509(callback))
|
|
// if err != nil {
|
|
// errs = append(errs, err)
|
|
// }
|
|
// }
|
|
// return errorFromErrors(errs)
|
|
//}
|
|
//
|
|
//// 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, string, error) error) error {
|
|
// errs := []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")
|
|
// }
|
|
//
|
|
// err = readCertsFromStream(reader, "", format, password, pemToX509(callback))
|
|
// if err != nil {
|
|
// errs = append(errs, err)
|
|
// }
|
|
// }
|
|
// return errorFromErrors(errs)
|
|
//}
|
|
//
|
|
//func pemToX509(callback func(*x509.Certificate, string, error) error) func(*pem.Block, string) error {
|
|
// return func(block *pem.Block, format string) error {
|
|
// switch block.Type {
|
|
// case "CERTIFICATE":
|
|
// cert, err := x509.ParseCertificate(block.Bytes)
|
|
// return callback(cert, format, err)
|
|
// case "PKCS7":
|
|
// certs, err := pkcs7.ExtractCertificates(block.Bytes)
|
|
// if err == nil {
|
|
// for _, cert := range certs {
|
|
// return callback(cert, format, nil)
|
|
// }
|
|
// } else {
|
|
// return callback(nil, format, err)
|
|
// }
|
|
// case "CERTIFICATE REQUEST":
|
|
// fmt.Println("warning: certificate requests are not supported")
|
|
// }
|
|
// return nil
|
|
// }
|
|
//}
|
|
//
|
|
//func ReadCertsFromStream(reader io.Reader, filename string, format string, password string, callback func(*pem.Block, string) error) error {
|
|
// passwordFunc := func(promet string) string {
|
|
// return password
|
|
// }
|
|
// return readCertsFromStream(reader, filename, format, passwordFunc, callback)
|
|
//}
|
|
|
|
// 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, string) error) error {
|
|
headers := map[string]string{}
|
|
if filename != "" && filename != os.Stdin.Name() {
|
|
headers[fileHeader] = filename
|
|
}
|
|
|
|
format = strings.TrimSpace(format)
|
|
switch format {
|
|
case "PEM":
|
|
scanner := pemScanner(reader)
|
|
for scanner.Scan() {
|
|
block, _ := pem.Decode(scanner.Bytes())
|
|
block.Headers = mergeHeaders(block.Headers, headers)
|
|
err := callback(block, format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case "DER":
|
|
data, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read input: %s\n", err)
|
|
}
|
|
x509Certs, err0 := x509.ParseCertificates(data)
|
|
if err0 == nil {
|
|
for _, cert := range x509Certs {
|
|
err := callback(encodeX509ToPEM(cert, headers), format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
p7bBlocks, err1 := pkcs7.ParseSignedData(data)
|
|
if err1 == nil {
|
|
for _, block := range p7bBlocks {
|
|
err := callback(pkcs7ToPem(block, headers), format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unable to parse certificates from DER data\n* X.509 parser gave: %s\n* PKCS7 parser gave: %s\n", err0, err1)
|
|
case "PKCS12":
|
|
data, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read input: %s\n", err)
|
|
}
|
|
blocks, err := pkcs12.ToPEM(data, password(""))
|
|
if err != nil || len(blocks) == 0 {
|
|
return fmt.Errorf("keystore appears to be empty or password was incorrect\n")
|
|
}
|
|
for _, block := range blocks {
|
|
block.Headers = mergeHeaders(block.Headers, headers)
|
|
err := callback(block, format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case "JCEKS":
|
|
keyStore, err := jceks.LoadFromReader(reader, []byte(password("")))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse keystore: %s\n", err)
|
|
}
|
|
for _, alias := range keyStore.ListCerts() {
|
|
cert, _ := keyStore.GetCert(alias)
|
|
err := callback(encodeX509ToPEM(cert, mergeHeaders(headers, map[string]string{nameHeader: alias})), format)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if err := callback(block, format); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, cert := range certs {
|
|
if err = callback(encodeX509ToPEM(cert, mergedHeaders), format); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
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...)
|
|
// TODO start with 'Certificate'
|
|
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
|
|
}
|