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 }