github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/security/certificate_loader.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package security
    12  
    13  import (
    14  	"context"
    15  	"crypto/x509"
    16  	"io/ioutil"
    17  	"os"
    18  	"path/filepath"
    19  	"runtime"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/cockroachdb/cockroach/pkg/util/envutil"
    24  	"github.com/cockroachdb/cockroach/pkg/util/log"
    25  	"github.com/cockroachdb/errors"
    26  )
    27  
    28  func init() {
    29  	if runtime.GOOS == "windows" {
    30  		// File modes on windows default to 0666 for r/w files:
    31  		// https://golang.org/src/os/types_windows.go?#L31
    32  		// This would fail any attempt to load keys, so we need to disable permission checks.
    33  		skipPermissionChecks = true
    34  	} else {
    35  		skipPermissionChecks = envutil.EnvOrDefaultBool("COCKROACH_SKIP_KEY_PERMISSION_CHECK", false)
    36  	}
    37  }
    38  
    39  var skipPermissionChecks bool
    40  
    41  // AssetLoader describes the functions necessary to read certificate and key files.
    42  type AssetLoader struct {
    43  	ReadDir  func(dirname string) ([]os.FileInfo, error)
    44  	ReadFile func(filename string) ([]byte, error)
    45  	Stat     func(name string) (os.FileInfo, error)
    46  }
    47  
    48  // defaultAssetLoader uses real filesystem calls.
    49  var defaultAssetLoader = AssetLoader{
    50  	ReadDir:  ioutil.ReadDir,
    51  	ReadFile: ioutil.ReadFile,
    52  	Stat:     os.Stat,
    53  }
    54  
    55  // assetLoaderImpl is used to list/read/stat security assets.
    56  var assetLoaderImpl = defaultAssetLoader
    57  
    58  // SetAssetLoader overrides the asset loader with the passed-in one.
    59  func SetAssetLoader(al AssetLoader) {
    60  	assetLoaderImpl = al
    61  }
    62  
    63  // ResetAssetLoader restores the asset loader to the default value.
    64  func ResetAssetLoader() {
    65  	assetLoaderImpl = defaultAssetLoader
    66  }
    67  
    68  // PemUsage indicates the purpose of a given certificate.
    69  type PemUsage uint32
    70  
    71  const (
    72  	_ PemUsage = iota
    73  	// CAPem describes the main CA certificate.
    74  	CAPem
    75  	// ClientCAPem describes the CA certificate used to verify client certificates.
    76  	ClientCAPem
    77  	// UICAPem describes the CA certificate used to verify the Admin UI server certificate.
    78  	UICAPem
    79  	// NodePem describes the server certificate for the node, possibly a combined server/client
    80  	// certificate for user Node if a separate 'client.node.crt' is not present.
    81  	NodePem
    82  	// UIPem describes the server certificate for the admin UI.
    83  	UIPem
    84  	// ClientPem describes a client certificate.
    85  	ClientPem
    86  
    87  	// Maximum allowable permissions.
    88  	maxKeyPermissions os.FileMode = 0700
    89  	// Filename extenstions.
    90  	certExtension = `.crt`
    91  	keyExtension  = `.key`
    92  	// Certificate directory permissions.
    93  	defaultCertsDirPerm = 0700
    94  )
    95  
    96  func isCA(usage PemUsage) bool {
    97  	return usage == CAPem || usage == ClientCAPem || usage == UICAPem
    98  }
    99  
   100  func (p PemUsage) String() string {
   101  	switch p {
   102  	case CAPem:
   103  		return "CA"
   104  	case ClientCAPem:
   105  		return "Client CA"
   106  	case UICAPem:
   107  		return "UI CA"
   108  	case NodePem:
   109  		return "Node"
   110  	case UIPem:
   111  		return "UI"
   112  	case ClientPem:
   113  		return "Client"
   114  	default:
   115  		return "unknown"
   116  	}
   117  }
   118  
   119  // CertInfo describe a certificate file and optional key file.
   120  // To obtain the full path, Filename and KeyFilename must be joined
   121  // with the certs directory.
   122  // The key may not be present if this is a CA certificate.
   123  // If Err != nil, the CertInfo must NOT be used.
   124  type CertInfo struct {
   125  	// FileUsage describes the use of this certificate.
   126  	FileUsage PemUsage
   127  
   128  	// Filename is the base filename of the certificate.
   129  	Filename string
   130  	// FileContents is the raw cert file data.
   131  	FileContents []byte
   132  
   133  	// KeyFilename is the base filename of the key, blank if not found (CA certs only).
   134  	KeyFilename string
   135  	// KeyFileContents is the raw key file data.
   136  	KeyFileContents []byte
   137  
   138  	// Name is the blob in the middle of the filename. eg: username for client certs.
   139  	Name string
   140  
   141  	// Parsed certificates. This is used by debugging/printing/monitoring only,
   142  	// TLS config objects are passed raw certificate file contents.
   143  	// CA certs may contain (and use) more than one certificate.
   144  	// Client/Server certs may contain more than one, but only the first certificate will be used.
   145  	ParsedCertificates []*x509.Certificate
   146  
   147  	// Expiration time is the latest "Not After" date across all parsed certificates.
   148  	ExpirationTime time.Time
   149  
   150  	// Error is any error encountered when loading the certificate/key pair.
   151  	// For example: bad permissions on the key will be stored here.
   152  	Error error
   153  }
   154  
   155  func exceedsPermissions(objectMode, allowedMode os.FileMode) bool {
   156  	mask := os.FileMode(0777) ^ allowedMode
   157  	return mask&objectMode != 0
   158  }
   159  
   160  func isCertificateFile(filename string) bool {
   161  	return strings.HasSuffix(filename, certExtension)
   162  }
   163  
   164  // CertInfoFromFilename takes a filename and attempts to determine the
   165  // certificate usage (ca, node, etc..).
   166  func CertInfoFromFilename(filename string) (*CertInfo, error) {
   167  	parts := strings.Split(filename, `.`)
   168  	numParts := len(parts)
   169  
   170  	if numParts < 2 {
   171  		return nil, errors.New("not enough parts found")
   172  	}
   173  
   174  	var fileUsage PemUsage
   175  	var name string
   176  	prefix := parts[0]
   177  	switch parts[0] {
   178  	case `ca`:
   179  		fileUsage = CAPem
   180  		if numParts != 2 {
   181  			return nil, errors.Errorf("CA certificate filename should match ca%s", certExtension)
   182  		}
   183  	case `ca-client`:
   184  		fileUsage = ClientCAPem
   185  		if numParts != 2 {
   186  			return nil, errors.Errorf("client CA certificate filename should match ca-client%s", certExtension)
   187  		}
   188  	case `ca-ui`:
   189  		fileUsage = UICAPem
   190  		if numParts != 2 {
   191  			return nil, errors.Errorf("UI CA certificate filename should match ca-ui%s", certExtension)
   192  		}
   193  	case `node`:
   194  		fileUsage = NodePem
   195  		if numParts != 2 {
   196  			return nil, errors.Errorf("node certificate filename should match node%s", certExtension)
   197  		}
   198  	case `ui`:
   199  		fileUsage = UIPem
   200  		if numParts != 2 {
   201  			return nil, errors.Errorf("UI certificate filename should match ui%s", certExtension)
   202  		}
   203  	case `client`:
   204  		fileUsage = ClientPem
   205  		// strip prefix and suffix and re-join middle parts.
   206  		name = strings.Join(parts[1:numParts-1], `.`)
   207  		if len(name) == 0 {
   208  			return nil, errors.Errorf("client certificate filename should match client.<user>%s", certExtension)
   209  		}
   210  	default:
   211  		return nil, errors.Errorf("unknown prefix %q", prefix)
   212  	}
   213  
   214  	return &CertInfo{
   215  		FileUsage: fileUsage,
   216  		Filename:  filename,
   217  		Name:      name,
   218  	}, nil
   219  }
   220  
   221  // CertificateLoader searches for certificates and keys in the certs directory.
   222  type CertificateLoader struct {
   223  	certsDir             string
   224  	skipPermissionChecks bool
   225  	certificates         []*CertInfo
   226  }
   227  
   228  // Certificates returns the loaded certificates.
   229  func (cl *CertificateLoader) Certificates() []*CertInfo {
   230  	return cl.certificates
   231  }
   232  
   233  // NewCertificateLoader creates a new instance of the certificate loader.
   234  func NewCertificateLoader(certsDir string) *CertificateLoader {
   235  	return &CertificateLoader{
   236  		certsDir:             certsDir,
   237  		skipPermissionChecks: skipPermissionChecks,
   238  		certificates:         make([]*CertInfo, 0),
   239  	}
   240  }
   241  
   242  // MaybeCreateCertsDir creates the certificate directory if it does not
   243  // exist. Returns an error if we could not stat or create the directory.
   244  func (cl *CertificateLoader) MaybeCreateCertsDir() error {
   245  	dirInfo, err := os.Stat(cl.certsDir)
   246  	if err == nil {
   247  		if !dirInfo.IsDir() {
   248  			return errors.Errorf("certs directory %s exists but is not a directory", cl.certsDir)
   249  		}
   250  		return nil
   251  	}
   252  
   253  	if !os.IsNotExist(err) {
   254  		return makeErrorf(err, "could not stat certs directory %s", cl.certsDir)
   255  	}
   256  
   257  	if err := os.Mkdir(cl.certsDir, defaultCertsDirPerm); err != nil {
   258  		return makeErrorf(err, "could not create certs directory %s", cl.certsDir)
   259  	}
   260  	return nil
   261  }
   262  
   263  // TestDisablePermissionChecks turns off permissions checks.
   264  // Used by tests only.
   265  func (cl *CertificateLoader) TestDisablePermissionChecks() {
   266  	cl.skipPermissionChecks = true
   267  }
   268  
   269  // Load examines all .crt files in the certs directory, determines their
   270  // usage, and looks for their keys.
   271  // It populates the certificates field.
   272  func (cl *CertificateLoader) Load() error {
   273  	fileInfos, err := assetLoaderImpl.ReadDir(cl.certsDir)
   274  	if err != nil {
   275  		if os.IsNotExist(err) {
   276  			// Directory does not exist.
   277  			if log.V(3) {
   278  				log.Infof(context.Background(), "missing certs directory %s", cl.certsDir)
   279  			}
   280  			return nil
   281  		}
   282  		return err
   283  	}
   284  
   285  	if log.V(3) {
   286  		log.Infof(context.Background(), "scanning certs directory %s", cl.certsDir)
   287  	}
   288  
   289  	// Walk the directory contents.
   290  	for _, info := range fileInfos {
   291  		filename := info.Name()
   292  		fullPath := filepath.Join(cl.certsDir, filename)
   293  
   294  		if info.IsDir() {
   295  			// Skip subdirectories.
   296  			if log.V(3) {
   297  				log.Infof(context.Background(), "skipping sub-directory %s", fullPath)
   298  			}
   299  			continue
   300  		}
   301  
   302  		if !isCertificateFile(filename) {
   303  			if log.V(3) {
   304  				log.Infof(context.Background(), "skipping non-certificate file %s", filename)
   305  			}
   306  			continue
   307  		}
   308  
   309  		// Build the info struct from the filename.
   310  		ci, err := CertInfoFromFilename(filename)
   311  		if err != nil {
   312  			log.Warningf(context.Background(), "bad filename %s: %v", fullPath, err)
   313  			continue
   314  		}
   315  
   316  		// Read the cert file contents.
   317  		fullCertPath := filepath.Join(cl.certsDir, filename)
   318  		certPEMBlock, err := assetLoaderImpl.ReadFile(fullCertPath)
   319  		if err != nil {
   320  			log.Warningf(context.Background(), "could not read certificate file %s: %v", fullPath, err)
   321  		}
   322  		ci.FileContents = certPEMBlock
   323  
   324  		// Parse certificate, then look for the private key.
   325  		// Errors are persisted for better visibility later.
   326  		if err := parseCertificate(ci); err != nil {
   327  			log.Warningf(context.Background(), "could not parse certificate for %s: %v", fullPath, err)
   328  			ci.Error = err
   329  		} else if err := cl.findKey(ci); err != nil {
   330  			log.Warningf(context.Background(), "error finding key for %s: %v", fullPath, err)
   331  			ci.Error = err
   332  		} else if log.V(3) {
   333  			log.Infof(context.Background(), "found certificate %s", ci.Filename)
   334  		}
   335  
   336  		cl.certificates = append(cl.certificates, ci)
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  // findKey takes a CertInfo and looks for the corresponding key file.
   343  // If found, sets the 'keyFilename' and returns nil, returns error otherwise.
   344  // Does not load CA keys.
   345  func (cl *CertificateLoader) findKey(ci *CertInfo) error {
   346  	if isCA(ci.FileUsage) {
   347  		return nil
   348  	}
   349  
   350  	keyFilename := strings.TrimSuffix(ci.Filename, certExtension) + keyExtension
   351  	fullKeyPath := filepath.Join(cl.certsDir, keyFilename)
   352  
   353  	// Stat the file. This follows symlinks.
   354  	info, err := assetLoaderImpl.Stat(fullKeyPath)
   355  	if err != nil {
   356  		return errors.Errorf("could not stat key file %s: %v", fullKeyPath, err)
   357  	}
   358  
   359  	// Only regular files are supported (after following symlinks).
   360  	fileMode := info.Mode()
   361  	if !fileMode.IsRegular() {
   362  		return errors.Errorf("key file %s is not a regular file", fullKeyPath)
   363  	}
   364  
   365  	if !cl.skipPermissionChecks {
   366  		// Check permissions bits.
   367  		filePerm := fileMode.Perm()
   368  		if exceedsPermissions(filePerm, maxKeyPermissions) {
   369  			return errors.Errorf("key file %s has permissions %s, exceeds %s",
   370  				fullKeyPath, filePerm, maxKeyPermissions)
   371  		}
   372  	}
   373  
   374  	// Read key file.
   375  	keyPEMBlock, err := assetLoaderImpl.ReadFile(fullKeyPath)
   376  	if err != nil {
   377  		return errors.Errorf("could not read key file %s: %v", fullKeyPath, err)
   378  	}
   379  
   380  	ci.KeyFilename = keyFilename
   381  	ci.KeyFileContents = keyPEMBlock
   382  	return nil
   383  }
   384  
   385  // parseCertificate attempts to parse the cert file contents into x509 certificate objects.
   386  // The Error field must be nil
   387  func parseCertificate(ci *CertInfo) error {
   388  	if ci.Error != nil {
   389  		return makeErrorf(ci.Error, "parseCertificate called on bad CertInfo object: %s", ci.Filename)
   390  	}
   391  
   392  	if len(ci.FileContents) == 0 {
   393  		return errors.Errorf("empty certificate file: %s", ci.Filename)
   394  	}
   395  
   396  	// PEM-decode the file.
   397  	derCerts, err := PEMToCertificates(ci.FileContents)
   398  	if err != nil {
   399  		return makeErrorf(err, "failed to parse certificate file %s as PEM", ci.Filename)
   400  	}
   401  
   402  	// Make sure we get at least one certificate.
   403  	if len(derCerts) == 0 {
   404  		return errors.Errorf("no certificates found in %s", ci.Filename)
   405  	}
   406  
   407  	certs := make([]*x509.Certificate, len(derCerts))
   408  	var expires time.Time
   409  	for i, c := range derCerts {
   410  		x509Cert, err := x509.ParseCertificate(c.Bytes)
   411  		if err != nil {
   412  			return makeErrorf(err, "failed to parse certificate %d in file %s", i, ci.Filename)
   413  		}
   414  
   415  		if i == 0 {
   416  			// Only check details of the first certificate.
   417  			if err := validateCockroachCertificate(ci, x509Cert); err != nil {
   418  				return makeErrorf(err, "failed to validate certificate %d in file %s", i, ci.Filename)
   419  			}
   420  
   421  			// Expiration from the first certificate.
   422  			expires = x509Cert.NotAfter
   423  		}
   424  		certs[i] = x509Cert
   425  	}
   426  
   427  	ci.ParsedCertificates = certs
   428  	ci.ExpirationTime = expires
   429  	return nil
   430  }
   431  
   432  // validateDualPurposeNodeCert takes a CertInfo and a parsed certificate and checks the
   433  // values of certain fields.
   434  // This should only be called on the NodePem CertInfo when there is no specific
   435  // client certificate for the 'node' user.
   436  // Fields required for a valid server certificate are already checked.
   437  func validateDualPurposeNodeCert(ci *CertInfo) error {
   438  	if ci == nil {
   439  		return errors.Errorf("no node certificate found")
   440  	}
   441  
   442  	if ci.Error != nil {
   443  		return ci.Error
   444  	}
   445  
   446  	// The first certificate is used in client auth.
   447  	cert := ci.ParsedCertificates[0]
   448  	principals := getCertificatePrincipals(cert)
   449  	if !ContainsUser(NodeUser, principals) {
   450  		return errors.Errorf("client/server node certificate has principals %q, expected %q",
   451  			principals, NodeUser)
   452  	}
   453  
   454  	return nil
   455  }
   456  
   457  // validateCockroachCertificate takes a CertInfo and a parsed certificate and checks the
   458  // values of certain fields.
   459  func validateCockroachCertificate(ci *CertInfo, cert *x509.Certificate) error {
   460  
   461  	switch ci.FileUsage {
   462  	case NodePem:
   463  		// Common Name is checked only if there is no client certificate for 'node'.
   464  		// This is done in validateDualPurposeNodeCert.
   465  	case ClientPem:
   466  		// Check that CommonName matches the username extracted from the filename.
   467  		principals := getCertificatePrincipals(cert)
   468  		if !ContainsUser(ci.Name, principals) {
   469  			return errors.Errorf("client certificate has principals %q, expected %q",
   470  				principals, ci.Name)
   471  		}
   472  	}
   473  	return nil
   474  }