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 }