github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/security/certificate_loader_test.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_test
    12  
    13  import (
    14  	"bytes"
    15  	"crypto/rand"
    16  	"crypto/rsa"
    17  	"crypto/x509"
    18  	"crypto/x509/pkix"
    19  	"encoding/pem"
    20  	"io/ioutil"
    21  	"math/big"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/cockroachdb/cockroach/pkg/security"
    29  	"github.com/cockroachdb/cockroach/pkg/security/securitytest"
    30  	"github.com/cockroachdb/cockroach/pkg/testutils"
    31  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    32  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    33  	"github.com/cockroachdb/errors"
    34  )
    35  
    36  func TestCertNomenclature(t *testing.T) {
    37  	defer leaktest.AfterTest(t)()
    38  
    39  	// We're just testing nomenclature parsing, all files exist and contain a valid PEM block.
    40  
    41  	testCases := []struct {
    42  		filename      string
    43  		expectedError string
    44  		usage         security.PemUsage
    45  		name          string
    46  	}{
    47  		// Test valid names.
    48  		{"ca.crt", "", security.CAPem, ""},
    49  		{"ca-client.crt", "", security.ClientCAPem, ""},
    50  		{"ca-ui.crt", "", security.UICAPem, ""},
    51  		{"node.crt", "", security.NodePem, ""},
    52  		{"ui.crt", "", security.UIPem, ""},
    53  		{"client.root.crt", "", security.ClientPem, "root"},
    54  		{"client.foo-bar.crt", "", security.ClientPem, "foo-bar"},
    55  		{"client....foo.bar.baz.how.many.dots.do.you.need...really....crt", "", security.ClientPem, "...foo.bar.baz.how.many.dots.do.you.need...really..."},
    56  
    57  		// Bad names. This function is only called on filenames ending with '.crt'.
    58  		{"crt", "not enough parts found", 0, ""},
    59  		{".crt", "unknown prefix", 0, ""},
    60  		{"ca2.crt", "unknown prefix \"ca2\"", 0, ""},
    61  		{"ca-client.foo.crt", "client CA certificate filename should match ca-client.crt", 0, ""},
    62  		{"ca-ui.foo.crt", "UI CA certificate filename should match ca-ui.crt", 0, ""},
    63  		{"ca.client.crt", "CA certificate filename should match ca.crt", 0, ""},
    64  		{"ui2.crt", "unknown prefix \"ui2\"", 0, ""},
    65  		{"ui.blah.crt", "UI certificate filename should match ui.crt", 0, ""},
    66  		{"node2.crt", "unknown prefix \"node2\"", 0, ""},
    67  		{"node.foo.crt", "node certificate filename should match node.crt", 0, ""},
    68  		{"client2.crt", "unknown prefix \"client2\"", 0, ""},
    69  		{"client.crt", "client certificate filename should match client.<user>.crt", 0, ""},
    70  		{"root.crt", "unknown prefix \"root\"", 0, ""},
    71  	}
    72  
    73  	for i, tc := range testCases {
    74  		ci, err := security.CertInfoFromFilename(tc.filename)
    75  		if !testutils.IsError(err, tc.expectedError) {
    76  			t.Errorf("#%d: expected error %v, got %v", i, tc.expectedError, err)
    77  			continue
    78  		}
    79  		if err != nil {
    80  			continue
    81  		}
    82  		if ci.FileUsage != tc.usage {
    83  			t.Errorf("#%d: expected file usage %v, got %v", i, tc.usage, ci.FileUsage)
    84  		}
    85  		if ci.Name != tc.name {
    86  			t.Errorf("#%d: expected name %v, got %v", i, tc.name, ci.Name)
    87  		}
    88  	}
    89  }
    90  
    91  func TestLoadEmbeddedCerts(t *testing.T) {
    92  	defer leaktest.AfterTest(t)()
    93  	cl := security.NewCertificateLoader(security.EmbeddedCertsDir)
    94  	if err := cl.Load(); err != nil {
    95  		t.Error(err)
    96  	}
    97  
    98  	assets, err := securitytest.AssetReadDir(security.EmbeddedCertsDir)
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  
   103  	// Check that we have "found pairs * 2 = num assets".
   104  	certs := cl.Certificates()
   105  	if act, exp := len(certs), len(assets); act*2 != exp {
   106  		t.Errorf("found %d keypairs, but have %d embedded files", act, exp)
   107  	}
   108  
   109  	// Check that all non-CA pairs include a key.
   110  	for _, c := range certs {
   111  		if c.FileUsage == security.CAPem {
   112  			if len(c.KeyFilename) != 0 {
   113  				t.Errorf("CA key was loaded for CertInfo %+v", c)
   114  			}
   115  		} else if len(c.KeyFilename) == 0 {
   116  			t.Errorf("no key found as part of CertInfo %+v", c)
   117  		}
   118  	}
   119  }
   120  
   121  func countLoadedCertificates(certsDir string) (int, error) {
   122  	cl := security.NewCertificateLoader(certsDir)
   123  	if err := cl.Load(); err != nil {
   124  		return 0, err
   125  	}
   126  	return len(cl.Certificates()), nil
   127  }
   128  
   129  // Generate a x509 cert with specific fields.
   130  func makeTestCert(
   131  	t *testing.T, commonName string, keyUsage x509.KeyUsage, extUsages []x509.ExtKeyUsage,
   132  ) (*x509.Certificate, []byte) {
   133  	// Make smallest rsa key possible: not saved.
   134  	key, err := rsa.GenerateKey(rand.Reader, 512)
   135  	if err != nil {
   136  		t.Fatalf("error on GenerateKey for CN=%s: %v", commonName, err)
   137  	}
   138  
   139  	// Specify the smallest possible set of fields.
   140  	template := &x509.Certificate{
   141  		SerialNumber: big.NewInt(1),
   142  		Subject: pkix.Name{
   143  			CommonName: commonName,
   144  		},
   145  		NotBefore: timeutil.Now().Add(-time.Hour),
   146  		NotAfter:  timeutil.Now().Add(time.Hour),
   147  		KeyUsage:  keyUsage,
   148  	}
   149  
   150  	template.ExtKeyUsage = extUsages
   151  
   152  	certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
   153  	if err != nil {
   154  		t.Fatalf("error on CreateCertificate for CN=%s: %v", commonName, err)
   155  	}
   156  
   157  	// parse it back.
   158  	parsedCert, err := x509.ParseCertificate(certBytes)
   159  	if err != nil {
   160  		t.Fatalf("error on ParseCertificate for CN=%s: %v", commonName, err)
   161  	}
   162  
   163  	certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
   164  	return parsedCert, pem.EncodeToMemory(certBlock)
   165  }
   166  
   167  func TestNamingScheme(t *testing.T) {
   168  	defer leaktest.AfterTest(t)()
   169  
   170  	fullKeyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature
   171  	// Build a few certificates. These are barebones since we only need to check our custom validation,
   172  	// not chain verification.
   173  	parsedCACert, caCert := makeTestCert(t, "", 0, nil)
   174  
   175  	parsedGoodNodeCert, goodNodeCert := makeTestCert(t, "node", fullKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
   176  	_, badUserNodeCert := makeTestCert(t, "notnode", fullKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
   177  
   178  	parsedGoodRootCert, goodRootCert := makeTestCert(t, "root", fullKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
   179  	_, notRootCert := makeTestCert(t, "notroot", fullKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
   180  
   181  	// Do not use embedded certs.
   182  	security.ResetAssetLoader()
   183  	defer ResetTest()
   184  
   185  	// Some test cases are skipped on windows due to non-UGO permissions.
   186  	isWindows := runtime.GOOS == "windows"
   187  
   188  	// Test non-existent directory.
   189  	// If the directory exists, we still expect no failures, unless it happens to contain
   190  	// valid filenames, so we don't need to try too hard to generate a unique name.
   191  	if count, err := countLoadedCertificates("my_non_existent_directory-only_for_tests"); err != nil {
   192  		t.Error(err)
   193  	} else if exp := 0; exp != count {
   194  		t.Errorf("found %d certificates, expected %d", count, exp)
   195  	}
   196  
   197  	// Create directory.
   198  	certsDir, err := ioutil.TempDir("", "certs_test")
   199  	if err != nil {
   200  		t.Fatal(err)
   201  	}
   202  	defer func() {
   203  		if err := os.RemoveAll(certsDir); err != nil {
   204  			t.Fatal(err)
   205  		}
   206  	}()
   207  
   208  	type testFile struct {
   209  		name     string
   210  		mode     os.FileMode
   211  		contents []byte
   212  	}
   213  
   214  	testData := []struct {
   215  		// Files to write (name and mode).
   216  		files []testFile
   217  		// Certinfos found. Ordered by cert base filename.
   218  		certs []security.CertInfo
   219  		// Set to true to skip permissions checks.
   220  		skipChecks bool
   221  		// Skip test case on windows as permissions are always ignored.
   222  		skipWindows bool
   223  	}{
   224  		{
   225  			// Empty directory.
   226  			files: []testFile{},
   227  			certs: []security.CertInfo{},
   228  		},
   229  		{
   230  			// Test bad names, including ca/node certs with blobs in the middle, wrong separator.
   231  			// We only need to test certs, if they're not loaded, neither will keys.
   232  			files: []testFile{
   233  				{"ca.foo.crt", 0777, []byte{}},
   234  				{"cr..crt", 0777, []byte{}},
   235  				{"node.foo.crt", 0777, []byte{}},
   236  				{"node..crt", 0777, []byte{}},
   237  				{"client.crt", 0777, []byte{}},
   238  				{"client..crt", 0777, []byte{}},
   239  			},
   240  			certs: []security.CertInfo{},
   241  		},
   242  		{
   243  			// Test proper names, but no key files, only the CA cert should be loaded without error.
   244  			files: []testFile{
   245  				{"ca.crt", 0777, caCert},
   246  				{"node.crt", 0777, goodNodeCert},
   247  				{"client.root.crt", 0777, goodRootCert},
   248  			},
   249  			certs: []security.CertInfo{
   250  				{FileUsage: security.CAPem, Filename: "ca.crt", FileContents: caCert},
   251  				{FileUsage: security.ClientPem, Filename: "client.root.crt", Name: "root",
   252  					Error: errors.New(".* no such file or directory")},
   253  				{FileUsage: security.NodePem, Filename: "node.crt",
   254  					Error: errors.New(".* no such file or directory")},
   255  			},
   256  		},
   257  		{
   258  			// Key files, but wrong permissions.
   259  			// We don't load CA keys here, so permissions for them don't matter.
   260  			files: []testFile{
   261  				{"ca.crt", 0777, caCert},
   262  				{"ca.key", 0777, []byte{}},
   263  				{"node.crt", 0777, goodNodeCert},
   264  				{"node.key", 0704, []byte{}},
   265  				{"client.root.crt", 0777, goodRootCert},
   266  				{"client.root.key", 0740, []byte{}},
   267  			},
   268  			certs: []security.CertInfo{
   269  				{FileUsage: security.CAPem, Filename: "ca.crt", FileContents: caCert},
   270  				{FileUsage: security.ClientPem, Filename: "client.root.crt", Name: "root",
   271  					Error: errors.New(".* exceeds -rwx------")},
   272  				{FileUsage: security.NodePem, Filename: "node.crt",
   273  					Error: errors.New(".* exceeds -rwx------")},
   274  			},
   275  			skipWindows: true,
   276  		},
   277  		{
   278  			// Bad CommonName: this is checked later in the CertificateManager.
   279  			files: []testFile{
   280  				{"node.crt", 0777, badUserNodeCert},
   281  				{"node.key", 0700, []byte("node.key")},
   282  				{"client.root.crt", 0777, notRootCert},
   283  				{"client.root.key", 0700, []byte{}},
   284  			},
   285  			certs: []security.CertInfo{
   286  				{FileUsage: security.ClientPem, Filename: "client.root.crt", Name: "root",
   287  					Error: errors.New(`client certificate has principals \["notroot"\], expected "root"`)},
   288  				{FileUsage: security.NodePem, Filename: "node.crt", KeyFilename: "node.key",
   289  					FileContents: badUserNodeCert, KeyFileContents: []byte("node.key")},
   290  			},
   291  		},
   292  		{
   293  			// Everything loads.
   294  			files: []testFile{
   295  				{"ca.crt", 0777, caCert},
   296  				{"ca.key", 0700, []byte("ca.key")},
   297  				{"node.crt", 0777, goodNodeCert},
   298  				{"node.key", 0700, []byte("node.key")},
   299  				{"client.root.crt", 0777, goodRootCert},
   300  				{"client.root.key", 0700, []byte("client.root.key")},
   301  			},
   302  			certs: []security.CertInfo{
   303  				{FileUsage: security.CAPem, Filename: "ca.crt", FileContents: caCert},
   304  				{FileUsage: security.ClientPem, Filename: "client.root.crt", KeyFilename: "client.root.key",
   305  					Name: "root", FileContents: goodRootCert, KeyFileContents: []byte("client.root.key")},
   306  				{FileUsage: security.NodePem, Filename: "node.crt", KeyFilename: "node.key",
   307  					FileContents: goodNodeCert, KeyFileContents: []byte("node.key")},
   308  			},
   309  		},
   310  		{
   311  			// Certificates contain the CA: everything loads.
   312  			files: []testFile{
   313  				{"ca.crt", 0777, caCert},
   314  				{"ca.key", 0700, []byte("ca.key")},
   315  				{"node.crt", 0777, append(goodNodeCert, caCert...)},
   316  				{"node.key", 0700, []byte("node.key")},
   317  				{"client.root.crt", 0777, append(goodRootCert, caCert...)},
   318  				{"client.root.key", 0700, []byte("client.root.key")},
   319  			},
   320  			certs: []security.CertInfo{
   321  				{FileUsage: security.CAPem, Filename: "ca.crt", FileContents: caCert},
   322  				{FileUsage: security.ClientPem, Filename: "client.root.crt", KeyFilename: "client.root.key",
   323  					Name: "root", FileContents: append(goodRootCert, caCert...), KeyFileContents: []byte("client.root.key"),
   324  					ParsedCertificates: []*x509.Certificate{parsedGoodRootCert, parsedCACert}},
   325  				{FileUsage: security.NodePem, Filename: "node.crt", KeyFilename: "node.key",
   326  					FileContents: append(goodNodeCert, caCert...), KeyFileContents: []byte("node.key"),
   327  					ParsedCertificates: []*x509.Certificate{parsedGoodNodeCert, parsedCACert}},
   328  			},
   329  		},
   330  		{
   331  			// Bad key permissions, but skip permissions checks.
   332  			files: []testFile{
   333  				{"ca.crt", 0777, caCert},
   334  				{"ca.key", 0777, []byte("ca.key")},
   335  				{"node.crt", 0777, goodNodeCert},
   336  				{"node.key", 0777, []byte("node.key")},
   337  				{"client.root.crt", 0777, goodRootCert},
   338  				{"client.root.key", 0777, []byte("client.root.key")},
   339  			},
   340  			certs: []security.CertInfo{
   341  				{FileUsage: security.CAPem, Filename: "ca.crt", FileContents: caCert},
   342  				{FileUsage: security.ClientPem, Filename: "client.root.crt", KeyFilename: "client.root.key",
   343  					Name: "root", FileContents: goodRootCert, KeyFileContents: []byte("client.root.key")},
   344  				{FileUsage: security.NodePem, Filename: "node.crt", KeyFilename: "node.key",
   345  					FileContents: goodNodeCert, KeyFileContents: []byte("node.key")},
   346  			},
   347  			skipChecks: true,
   348  		},
   349  	}
   350  
   351  	for testNum, data := range testData {
   352  		if data.skipWindows && isWindows {
   353  			continue
   354  		}
   355  
   356  		// Write all files.
   357  		for _, f := range data.files {
   358  			n := f.name
   359  			if err := ioutil.WriteFile(filepath.Join(certsDir, n), f.contents, f.mode); err != nil {
   360  				t.Fatalf("#%d: could not write file %s: %v", testNum, n, err)
   361  			}
   362  		}
   363  
   364  		// Load certs.
   365  		cl := security.NewCertificateLoader(certsDir)
   366  		if data.skipChecks {
   367  			cl.TestDisablePermissionChecks()
   368  		}
   369  		if err := cl.Load(); err != nil {
   370  			t.Errorf("#%d: unexpected error: %v", testNum, err)
   371  		}
   372  
   373  		// Check count of certificates.
   374  		if expected, actual := len(data.certs), len(cl.Certificates()); expected != actual {
   375  			t.Errorf("#%d: expected %d certificates, found %d", testNum, expected, actual)
   376  		}
   377  
   378  		// Check individual certificates.
   379  		for i, actual := range cl.Certificates() {
   380  			expected := data.certs[i]
   381  
   382  			if expected.Error == nil {
   383  				if actual.Error != nil {
   384  					t.Errorf("#%d: expected success, got error: %+v", testNum, actual.Error)
   385  					continue
   386  				}
   387  			} else {
   388  				if !testutils.IsError(actual.Error, expected.Error.Error()) {
   389  					t.Errorf("#%d: mismatched error, expected: %+v, got %+v", testNum, expected.Error, actual.Error)
   390  				}
   391  				continue
   392  			}
   393  
   394  			// Compare some fields.
   395  			if actual.FileUsage != expected.FileUsage ||
   396  				actual.Filename != expected.Filename ||
   397  				actual.KeyFilename != expected.KeyFilename ||
   398  				actual.Name != expected.Name {
   399  				t.Errorf("#%d: mismatching CertInfo, expected: %+v, got %+v", testNum, expected, actual)
   400  				continue
   401  			}
   402  			if actual.Filename != "" {
   403  				if !bytes.Equal(actual.FileContents, expected.FileContents) {
   404  					t.Errorf("#%d: bad file contents: expected %s, got %s", testNum, expected.FileContents, actual.FileContents)
   405  					continue
   406  				}
   407  				if expected.ParsedCertificates != nil {
   408  					// ParsedCertificates was specified in the expected test output, check against it.
   409  					if a, e := len(actual.ParsedCertificates), len(expected.ParsedCertificates); a != e {
   410  						t.Errorf("#%d: expected %d certificates, found: %d", testNum, e, a)
   411  						continue
   412  					}
   413  					for certIndex := range actual.ParsedCertificates {
   414  						if a, e := actual.ParsedCertificates[certIndex], expected.ParsedCertificates[certIndex]; !a.Equal(e) {
   415  							t.Errorf("#%d: certificate %d does not match: got %v, expected %v", testNum, certIndex, a, e)
   416  							continue
   417  						}
   418  					}
   419  				} else {
   420  					// No ParsedCertificates specified, we expect just 1.
   421  					if a, e := len(actual.ParsedCertificates), 1; a != e {
   422  						t.Errorf("#%d: expected %d certificates, found: %d", testNum, e, a)
   423  						continue
   424  					}
   425  				}
   426  			}
   427  			if actual.KeyFilename != "" && !bytes.Equal(actual.KeyFileContents, expected.KeyFileContents) {
   428  				t.Errorf("#%d: bad file contents: expected %s, got %s", testNum, expected.KeyFileContents, actual.KeyFileContents)
   429  				continue
   430  			}
   431  		}
   432  
   433  		// Wipe all files.
   434  		for _, f := range data.files {
   435  			n := f.name
   436  			if err := os.Remove(filepath.Join(certsDir, n)); err != nil {
   437  				t.Fatalf("#%d: could not delete file %s: %v", testNum, n, err)
   438  			}
   439  		}
   440  	}
   441  }