package diagnose import ( "bytes" "context" "crypto/tls" "crypto/x509" "encoding/pem" "fmt" "io/ioutil" "strings" "time" "github.com/hashicorp/go-secure-stdlib/tlsutil" "github.com/hashicorp/vault/internalshared/configutil" ) const ( minVersionError = "'tls_min_version' value %q not supported, please specify one of [tls10,tls11,tls12,tls13]" maxVersionError = "'tls_max_version' value %q not supported, please specify one of [tls10,tls11,tls12,tls13]" ) // ListenerChecks diagnoses warnings and the first encountered error for the listener // configuration stanzas. func ListenerChecks(ctx context.Context, listeners []*configutil.Listener) ([]string, []error) { testName := "Check Listener TLS" ctx, span := StartSpan(ctx, testName) defer span.End() // These aggregated warnings and errors are returned purely for testing purposes. // The errors and warnings will report in this function itself. var listenerWarnings []string var listenerErrors []error for _, l := range listeners { listenerID := l.Address if l.TLSDisable { Warn(ctx, fmt.Sprintf("Listener at address %s: TLS is disabled in a listener config stanza.", listenerID)) continue } if l.TLSDisableClientCerts { Warn(ctx, fmt.Sprintf("Listener at address %s: TLS for a listener is turned on without requiring client certificates.", listenerID)) } status, warning := TLSMutualExclusionCertCheck(l) if status == 1 { Warn(ctx, warning) } // Perform the TLS version check for listeners. if l.TLSMinVersion == "" { l.TLSMinVersion = "tls12" } if l.TLSMaxVersion == "" { l.TLSMaxVersion = "tls13" } _, ok := tlsutil.TLSLookup[l.TLSMinVersion] if !ok { err := fmt.Errorf("Listener at address %s: %s.", listenerID, fmt.Sprintf(minVersionError, l.TLSMinVersion)) listenerErrors = append(listenerErrors, err) Fail(ctx, err.Error()) } _, ok = tlsutil.TLSLookup[l.TLSMaxVersion] if !ok { err := fmt.Errorf("Listener at address %s: %s.", listenerID, fmt.Sprintf(maxVersionError, l.TLSMaxVersion)) listenerErrors = append(listenerErrors, err) Fail(ctx, err.Error()) } // Perform checks on the TLS Cryptographic Information. warnings, err := TLSFileChecks(l.TLSCertFile, l.TLSKeyFile) listenerWarnings, listenerErrors = outputError(ctx, warnings, listenerWarnings, err, listenerErrors, listenerID) // Perform checks on the Client CA Cert warnings, err = TLSClientCAFileCheck(l) listenerWarnings, listenerErrors = outputError(ctx, warnings, listenerWarnings, err, listenerErrors, listenerID) // TODO: Use listenerutil.TLSConfig to warn on incorrect protocol specified // Alternatively, use tlsutil.SetupTLSConfig. } return listenerWarnings, listenerErrors } func outputError(ctx context.Context, newWarnings, listenerWarnings []string, newErr error, listenerErrors []error, listenerID string) ([]string, []error) { for _, warning := range newWarnings { warning = listenerID + ": " + warning listenerWarnings = append(listenerWarnings, warning) Warn(ctx, warning) } if newErr != nil { errMsg := listenerID + ": " + newErr.Error() listenerErrors = append(listenerErrors, fmt.Errorf(errMsg)) Fail(ctx, errMsg) } return listenerWarnings, listenerErrors } // TLSFileChecks returns an error and warnings after checking TLS information func TLSFileChecks(certpath, keypath string) ([]string, error) { warnings, err := TLSCertCheck(certpath) if err != nil { return warnings, err } // Utilize the native TLS Loading mechanism to ensure we have missed no errors _, err = tls.LoadX509KeyPair(certpath, keypath) return warnings, err } // TLSCertCheck returns an error and warning after checking TLS information on the given cert func TLSCertCheck(certpath string) ([]string, error) { // Parse TLS Certs from the certpath leafCerts, interCerts, rootCerts, err := ParseTLSInformation(certpath) if err != nil { return nil, err } // Check for TLS Warnings warnings, err := TLSFileWarningChecks(leafCerts, interCerts, rootCerts) if err != nil { return warnings, err } // Check for TLS Errors if err = TLSErrorChecks(leafCerts, interCerts, rootCerts); err != nil { return warnings, err } return warnings, err } // ParseTLSInformation parses certficate information and returns it from a cert path. func ParseTLSInformation(certFilePath string) ([]*x509.Certificate, []*x509.Certificate, []*x509.Certificate, error) { leafCerts := []*x509.Certificate{} interCerts := []*x509.Certificate{} rootCerts := []*x509.Certificate{} data, err := ioutil.ReadFile(certFilePath) if err != nil { return leafCerts, interCerts, rootCerts, fmt.Errorf("Failed to read certificate file: %w.", err) } certBlocks := []*pem.Block{} rst := []byte(data) for len(rst) != 0 { block, rest := pem.Decode(rst) if block == nil { return leafCerts, interCerts, rootCerts, fmt.Errorf("Could not decode certificate in certificate file.") } certBlocks = append(certBlocks, block) rst = rest } if len(certBlocks) == 0 { return leafCerts, interCerts, rootCerts, fmt.Errorf("No certificates found in certificate file.") } for _, certBlock := range certBlocks { cert, err := x509.ParseCertificate(certBlock.Bytes) if err != nil { return leafCerts, interCerts, rootCerts, fmt.Errorf("A PEM block does not parse to a certificate: %w.", err) } // Detect if the certificate is a root, leaf, or intermediate if cert.IsCA && bytes.Equal(cert.RawIssuer, cert.RawSubject) { // It's a root rootCerts = append(rootCerts, cert) } else if cert.IsCA { // It's not a root but it's a CA, so it's an inter interCerts = append(interCerts, cert) } else { // It's gotta be a leaf leafCerts = append(leafCerts, cert) } } return leafCerts, interCerts, rootCerts, nil } // TLSErrorChecks contains manual error checks against the TLS configuration func TLSErrorChecks(leafCerts, interCerts, rootCerts []*x509.Certificate) error { // Make sure there's the proper number of leafCerts. If there are multiple, it's a bad pem file. if len(leafCerts) == 0 { return fmt.Errorf("No leaf certificates detected.") } // First, create root pools and interPools from the root and inter certs lists rootPool := x509.NewCertPool() interPool := x509.NewCertPool() for _, root := range rootCerts { rootPool.AddCert(root) } for _, inter := range interCerts { interPool.AddCert(inter) } var err error // Verify checks that certificate isn't expired, is of correct usage type, and has an appropriate // chain. We start with Root for _, root := range rootCerts { _, err = root.Verify(x509.VerifyOptions{ Roots: rootPool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }) if err != nil { return fmt.Errorf("Failed to verify root certificate: %w.", err) } } // Verifying intermediate certs for _, inter := range interCerts { _, err = inter.Verify(x509.VerifyOptions{ Roots: rootPool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }) if err != nil { return fmt.Errorf("Failed to verify intermediate certificate: %w.", err) } } rootSubjs := rootPool.Subjects() if len(rootSubjs) == 0 && len(leafCerts) > 0 { // this is a self signed server certificate, or the root is just not provided. In any // case, we need to bypass the root verification step by adding the leaf itself to the // root pool. rootPool.AddCert(leafCerts[0]) } // Verifying leaf cert for _, leaf := range leafCerts { _, err = leaf.Verify(x509.VerifyOptions{ Roots: rootPool, Intermediates: interPool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }) if err != nil { return fmt.Errorf("Failed to verify primary provided leaf certificate: %w.", err) } } return nil } // TLSFileWarningChecks returns warnings based on the leaf certificates, intermediate certificates, // and root certificates provided. func TLSFileWarningChecks(leafCerts, interCerts, rootCerts []*x509.Certificate) ([]string, error) { var warnings []string // add a warning for when there are more than one leaf certs if len(leafCerts) > 1 { warnings = append(warnings, fmt.Sprintf("More than one leaf certificate detected. Please ensure that there is one unique leaf certificate being supplied to Vault in the Vault server configuration file.")) } for _, c := range leafCerts { if willExpire, timeToExpiry := NearExpiration(c); willExpire { warnings = append(warnings, fmt.Sprintf("Leaf certificate %d is expired or near expiry. Time to expire is: %s.", c.SerialNumber, timeToExpiry)) } } for _, c := range interCerts { if willExpire, timeToExpiry := NearExpiration(c); willExpire { warnings = append(warnings, fmt.Sprintf("Intermediate certificate %d is expired or near expiry. Time to expire is: %s.", c.SerialNumber, timeToExpiry)) } } for _, c := range rootCerts { if willExpire, timeToExpiry := NearExpiration(c); willExpire { warnings = append(warnings, fmt.Sprintf("Root certificate %d is expired or near expiry. Time to expire is: %s.", c.SerialNumber, timeToExpiry)) } } return warnings, nil } // NearExpiration returns a true if a certficate will expire in a month and false otherwise func NearExpiration(c *x509.Certificate) (bool, time.Duration) { oneMonthFromNow := time.Now().Add(30 * 24 * time.Hour) var timeToExpiry time.Duration if oneMonthFromNow.After(c.NotAfter) { timeToExpiry := oneMonthFromNow.Sub(c.NotAfter) return true, timeToExpiry } return false, timeToExpiry } // TLSMutualExclusionCertCheck returns error if both TLSDisableClientCerts and TLSRequireAndVerifyClientCert are set func TLSMutualExclusionCertCheck(l *configutil.Listener) (int, string) { if l.TLSDisableClientCerts { if l.TLSRequireAndVerifyClientCert { return 1, "The tls_disable_client_certs and tls_require_and_verify_client_cert fields in the listener stanza of the Vault server configuration are mutually exclusive fields. Please ensure they are not both set to true." } } return 0, "" } // TLSClientCAFileCheck Checks the validity of a client CA file func TLSClientCAFileCheck(l *configutil.Listener) ([]string, error) { if l.TLSDisableClientCerts { return nil, nil } else if !l.TLSRequireAndVerifyClientCert { return nil, nil } return TLSCAFileCheck(l.TLSClientCAFile) } // TLSCAFileCheck checks the validity of a TLS CA file func TLSCAFileCheck(CAFilePath string) ([]string, error) { var warningsSlc []string // Parse TLS Certs from the tls config leafCerts, interCerts, rootCerts, err := ParseTLSInformation(CAFilePath) if err != nil { return nil, err } if len(rootCerts) == 0 { return nil, fmt.Errorf("No root certificate found in CA certificate file.") } if len(rootCerts) > 1 { warningsSlc = append(warningsSlc, fmt.Sprintf("Found multiple root certificates in CA Certificate file instead of just one.")) } // Checking for Self-Signed cert and return an explicit error about it. // Self-Signed certs are placed in the leafCerts slice when parsed. if len(leafCerts) > 0 && !leafCerts[0].IsCA && bytes.Equal(leafCerts[0].RawIssuer, leafCerts[0].RawSubject) { warningsSlc = append(warningsSlc, "Found a self-signed certificate in the CA certificate file.") } if len(interCerts) > 0 { warningsSlc = append(warningsSlc, "Found at least one intermediate certificate in the CA certificate file.") } if len(leafCerts) > 0 { warningsSlc = append(warningsSlc, "Found at least one leaf certificate in the CA certificate file.") } var warnings []string // Check for TLS Warnings warnings, err = TLSFileWarningChecks(leafCerts, interCerts, rootCerts) for i, warning := range warnings { warnings[i] = strings.ReplaceAll(warning, "leaf", "root") } warningsSlc = append(warningsSlc, warnings...) if err != nil { return warningsSlc, err } // Adding rootCerts to leafCert to perform verification in TLSErrorChecks leafCerts = append(leafCerts, rootCerts[0]) // Check for TLS Errors if err = TLSErrorChecks(leafCerts, interCerts, rootCerts); err != nil { return warningsSlc, fmt.Errorf(strings.ReplaceAll(err.Error(), "leaf", "root")) } return warningsSlc, err }