open-vault/vault/diagnose/tls_verification.go

356 lines
12 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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
}